iOS 语音播报解决方案(实现支付宝/微信语音收款提示功能)

iOS10 语音播报填坑详解(解决串行播报中断问题)

在来聊这类需求的解决方案之前,咱们还是先来聊一聊这类需求的真实使用场景:语音播报。语音播报需求运用最为广泛的应该是收银对账了,就类似于支付宝、微信、收钱吧等的收款语音提示一样。在iOS 10 之前,苹果没有提供通知扩展类的时候,如果想要实现杀进程也可以正常播报语音消息很难,从ios 10添加了这一个通知扩展类后,实现杀进程播报语音就相对简单很多了。

我们先来看一个陌生的Tagget

  • Notification Service Extension

这个Notification Service Extension 就是苹果在 iOS 10的新系统中为我们添加的新特性,这个新特性就能帮助我们用来解决杀死进程正常语音播报

原理流程图

苹果官方解释:UNNotificationServiceExtension

详细步骤

  • 创建一个通知扩展类
  • 添加语音播报逻辑代码
  • 设置支持后台播放
  • iOS10 以下系统如何实现串行播报

创建一个通知扩展类

  • 首先我点击 Xcode 的 File -> New -> Target -> Notification Service Extension,新建一个通知扩展类Target。
image
image

新建完后,我们的工程会多出一个文件夹,这里示例Demo的Target命名为 NotificationSE,文件夹中有NotificationService.h NotificationService.m 文件,这两个文件就是后面我们要用到的通知扩展类文件

image

在没有对NotificationService做任何修改时,我们先来预览下 .m 文件中都有哪些内容

image

从上面的截图,我们可以看到,.m 文件其实很简单,就 2 个函数,其实后面我们对这个文件做逻辑处理,也是很简单的。

添加语音播报逻辑代码

  • 注意,这里我们使用的语音合成和播报组件也是苹果官方提供的组件,AVSpeechSynthesizerAVSpeechSynthesisVoiceAVSpeechUtterance

我们先来看下一段语音播放代码片段:

    AVSpeechSynthesizer *av = [[AVSpeechSynthesizer alloc] init];
    AVSpeechSynthesisVoice *voice = [AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"];
    AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:@"我是测试文案"];
    utterance.rate = 0.5;
    utterance.voice= voice;
    [av speakUtterance:utterance];

现在我们将 NotificationService .m 文件做修改,使之支持语音播报。并且能支持多条通知同时过来的串行播报。完整文件如下:

//
//  NotificationService.m
//  NotificationSE
//
//  Created by 刘光强 on 2018/9/17.
//  Copyright © 2018年 quangqiang. All rights reserved.
//

#import "NotificationService.h"
#import <MediaPlayer/MediaPlayer.h>
#import <AVFoundation/AVFoundation.h>

@interface NotificationService ()<AVSpeechSynthesizerDelegate>

@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;

@property (nonatomic, strong) AVSpeechSynthesisVoice *synthesisVoice;
@property (nonatomic, strong) AVSpeechSynthesizer *synthesizer;
@end

@implementation NotificationService

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    
    // 这个info 内容就是通知信息携带的数据,后面我们取语音播报的文案,通知栏的title,以及通知内容都是从这个info字段中获取
    NSDictionary *info = self.bestAttemptContent.userInfo;
    
    // 播报语音
    [self playVoiceWithContent: info[@"content"]];
    
    // 这行代码需要注释,当我们想解决当同时推送了多条消息,这时我们想多条消息一条一条的挨个播报,我们就需要将此行代码注释
//    self.contentHandler(self.bestAttemptContent);
}

- (void)playVoiceWithContent:(NSString *)content {
    AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:content];
    utterance.rate = 0.5;
    utterance.voice = self.synthesisVoice;
    [self.synthesizer speakUtterance:utterance];
}

// 新增语音播放代理函数,在语音播报完成的代理函数中,我们添加下面的一行代码
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance {
    self.contentHandler(self.bestAttemptContent);
}

- (void)serviceExtensionTimeWillExpire {
    // Called just before the extension will be terminated by the system.
    // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
    self.contentHandler(self.bestAttemptContent);
}

- (AVSpeechSynthesisVoice *)synthesisVoice {
    if (!_synthesisVoice) {
        _synthesisVoice = [AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"];
    }
    return _synthesisVoice;
}

- (AVSpeechSynthesizer *)synthesizer {
    if (!_synthesizer) {
        _synthesizer = [[AVSpeechSynthesizer alloc] init];
        _synthesizer.delegate = self;
    }
    return _synthesizer;
}

@end

下面我们来逐一对这个 .m 文件中的每一个函数做下解释

  • - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {}

这个函数是通知扩展类的最为核心的函数了,你可以理解为这个就是接受到苹果APNS 通知的一个钩子函数,每次当推送一条通知过来,都会执行到这个函数体内,所以说我们的语音播报逻辑也是在这个钩子函数中进行处理的。

  • - (void)playVoiceWithContent:(NSString *)content {}

这个函数很简单了,就是我们抽离出来的进行语音合成并播放出语音的函数,我们传递一个语音文案作为此函数的参数即可。

*- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance {}

这个函数就是我们今天的主角了,我们之所以能够实现当同时有多条通知同时推送,我们还能够一条一条的串行逐条播放,主要的功能就归功到这个函数了,这个函数是 AVSpeechSynthesizer 类的代理函数,就是一段语音播放完成后执行这个函数,每次当一条语音播放完成,都会被此函数勾住,我们在函数体内实现我们的处理逻辑。

  • - (void)serviceExtensionTimeWillExpire {}

此函数是扩展类自带的一个函数,从这个函数解释我们可以看出,这个函数是当扩展被系统终止之前,会调用到这个函数。

好了,.m文件的几个关键的函数我们都做了相应的解释了,可能还有些小伙伴不是很明白,这些和解决通知串行逐一播报有什么关系尼,下面我就来根据自己的经验给大家做下详细的解释。

先来说下苹果通知的通知栏问题

在苹果通知中,当来一条通知时,我们的手机会叮一下,然后手机通知栏弹出通知。这里大家注意下,其实这个叮一下出来的通知栏也是有生命周期的。从通知栏被弹出来,到通知栏最终被收起,其实中间苹果给了限制时间,大概就6秒左右的时长

说到6秒左右的时长,对于那些多条通知同时到达,需要串行来逐一播报,但是很多小伙伴们会遇到这样一个问题:就是当同时来了多条通知,总是只能播报2-3条,然后就语音中断了,后面的通知不会播报了,遇到这些问题的小伙伴们有没有注意到,其实只能播报2-3条,这个时间差其实就是6秒左右,也就是通知栏的生命周期时长。

出现上面的问题的原因就是:当第一条通知来了,弹出通知栏,然后开始播报第一条语音,第一条播报完了,开始播报第二天语音,可能当第二天语音播报到一半了,但是这个时候,通知栏周期的时间到了,这时通知栏就会收起,注意:,当通知栏收起时,扩展类里面的代码就会终止执行,导致后面的语音播报终端。

上面说到当通知栏收起时,扩展类的代码会终止执行,这里又引出了另一个注意点:就是我们创建的这个扩展类也是有生命周期的,并且这个生命周期和通知栏的生命周期他们是有依赖关系的。即:当通知栏收起时,扩展类就会被系统终止,扩展内里面的代码也会终止执行,只有当下一个通知栏弹出来,扩展类就恢复功能

上面说到通知栏的出现和收起能够影响到扩展类的功能,那我们是不是控制好通知栏的显示和隐藏,就能解决多条串行问题尼?

是的,我们只要控制好通知栏,就可以解决上面的棘手问题,那么问题又来了,我们怎么才能控制通知栏的显示和隐藏尼?感觉我们平时使用苹果的推送,从来没有关心过处理通知栏的显示与隐藏,感觉从来没有这样用过,是的,对应普通的需求,我们确实不需要关系通知栏显示隐藏,感觉这些苹果系统自己已经处理好了,通知来了就显示通知栏,等5秒左右,周期结束就隐藏通知栏。

其实啊,在扩展类里面中,苹果已经给我们指出了如何控制通知栏的显示和隐藏,核心就是这行代码:self.contentHandler(self.bestAttemptContent);,当我们调用到这行代码,就是用来弹出通知栏的,通知栏的隐藏不需要我们来控制了,因为5秒左右的生命周期结束后,它会自动隐藏。

是不是对这样代码既熟悉有陌生啊,熟悉是因为你的扩展类文件中确实有这行代码,陌生是因为你之前从来都没有用过这行代码,不知道行代码是用来干啥的。

好了,既然self.contentHandler(self.bestAttemptContent); 这行核心代码引用出来了,我们就回到最开始的问题,在没有做任何处理时,为什么当同时来多条通知是,语音播报就不能逐一播报尼,其实就是因为当每一条通知到达都会执行这个函数- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {},有没有发现,这个函数体里面 默认就是 执行了 self.contentHandler(self.bestAttemptContent); 这行代码。

假设 一次性同时来了 10条 通知,就会一次性调用了 10次 didReceiveNotificationRequest这个函数, 也就 执行了 10次 self.contentHandler(self.bestAttemptContent), 按照上面的说法,同时执行10次,不就是同时弹出10次的 通知栏吗,这里我调试时发现,当同时来10条通知时,通知栏并没有同时弹出来10次,可能只弹出来1-2次。也就只能在这1-2次的时间长度中进行语音播报了。

上面解释这么多,那么我们到底该如何做尼,细心的同学发现了,我们上面 贴出来的 .m 代码中,我们新增了一个 AVSpeechSynthesizer 类的代理函数,就是语音播报完成的函数,我们将 呼出通知栏的代码 self.contentHandler(self.bestAttemptContent); 添加到这个代理函数中。意思就是:当第一条语音播放完成了,这时我们呼出通知栏显示播放的内容(通知栏的周期时间大概6秒左右),正好这时可以播放第二条语音,等第二条语音播放完成了,呼出第二个通知的通知栏,继续播放第三天语音,以此类推。

看到这里,想必大家应该都理解了为啥之前总是语音播报中断的问题。

还有一个很重要的函数:- (void)serviceExtensionTimeWillExpire{},我们上面只是提了下,具体他具体有什么功能尼?

我们发现serviceExtensionTimeWillExpire函数中,也调用了 self.contentHandler(self.bestAttemptContent) 这行代码,它为啥也要调用这行代码尼?

这是因为:当我们在接受通知的钩子函数中(didReceiveNotificationRequest)没有调用self.contentHandler(self.bestAttemptContent) 这行代码,这时就会出现一个现象:就是通知收到了,但是没有通知栏出现,这时苹果就不允许了。苹果规定,当一条通知达到后,如果在30秒内,还没有呼出通知栏,我就系统强制调用self.contentHandler(self.bestAttemptContent) 来呼出通知栏。 这时想必大家都知道 serviceExtensionTimeWillExpire 函数的用途了吧

设置支持后台播放

  • 配置应用支持后台播放,这个只需要在Xcode中做下配置即可
image

这里需要注意:当勾上上面的配置后,可能会导致苹果审核不通过,这里我们可以在应用中添加一个语音播放的功能,并录制视频告知苹果用途,可能会过审。

iOS 10以下实现串行播报

核心代码如下


// 监听通知函数中调用添加数据到队列
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler {
   
   [self addOperation: @"语音文案"];
}

#pragma mark -队列管理推送通知
- (void)addOperation:(NSString *)title {
    [[self mainQueue] addOperation:[self customOperation:title]];
}

- (NSOperationQueue *)mainQueue {
    return [NSOperationQueue mainQueue];
}

- (NSOperation *)customOperation:(NSString *)content {
    
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        AVSpeechUtterance *utterance = nil;
        @autoreleasepool {
            utterance = [AVSpeechUtterance speechUtteranceWithString:content];
            utterance.rate = 0.5;
        }
        utterance.voice = self.voiceConfig;
        [self.synthConfig speakUtterance:utterance];
    }];
    return operation;
}

- (AVSpeechSynthesisVoice *)voiceConfig {
    if (_voiceConfig == nil) {
        _voiceConfig = [AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"];
    }
    return _voiceConfig;
}

- (AVSpeechSynthesizer *)synthConfig {
    if (_synthConfig == nil) {
        _synthConfig = [[AVSpeechSynthesizer alloc] init];
    }
    return _synthConfig;
}

注意事项

  • 上面的通知扩展类最低支持iOS系统为 10及10 以上,所以所 iOS10以下的系统,是不支持使用通知扩展的
  • 通知扩展文件中是不支持断点调试的,网上有说通过配置可以进行断点,可是我尝试了 很多次,还是不能断点,这里我的处理方式是,通过使用 临时的语音播报来代替断点,在需要断点的地方加一个语音播放,如果播报出来了,代表执行了此行
  • 上面我们介绍了speechSynthesizer:didFinishSpeechUtterance 语音播放完成的代理函数,可能有的小伙伴会遇到这个代理函数不执行的情况,这时我们需要将 AVSpeechSynthesizer 类的对象设置成全局属性即可。
  • iOS 10 以下的系统,我们也想实现同时多条通知的串行播报该怎么实现尼,我自己的做法是自己维护一个数组队列,具体的实现参照下面代码块。
  • content-avilable 字段的值,需要配置为 1
  • 添加支持后天播放时,可能会被苹果拒审
  • 如何实现扩展类和主工程之间的数据通信(这块内容会单独的出一篇文章来介绍)
  • 待补充

示例Demo

https://github.com/guangqiang-liu/iOS-NotificationExtensionDemo

总结

我们公司之前做的扫码支付需求,支付成功后播报支付金额,当时在开发这块需求时,遇到了杀进程无法进行语音播报的问题,后面引入了iOS10 的通知扩展类来解决杀进程问题。在使用扩展类时,也是遇到了不少的问题和大坑,这里就逐一做了下总结,上面的讲解也是填坑后的个人理解,如有错误之处,欢迎留言交流指出错误。

更多文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,158评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,600评论 1 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,785评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,655评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,075评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,002评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,146评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,918评论 0 211
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,671评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,838评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,318评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,636评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,343评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,187评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,982评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,126评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,934评论 2 279

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,937评论 3 118
  • 昆明梁艳分享129天。网课初级五期。2017.09.23 考虑了很久,都没有定下到底分享什么,主要是今...
    诗心小鹿阅读 199评论 0 1
  • 今年的初九别样冷清。二姐三姐回去上班了,姐姐姐夫早上倒是在家,但是下午就要走了,因为姐夫明天上班。 爸爸听见烟花的...
    辰方乙羲阅读 404评论 0 1
  • 杜月笙的大名如雷贯耳,早些时候办公室同事也提到此人,一直想阅读有关他的书籍。遗憾的是,当时在豆瓣里搜索了有关此人书...
    笨笨花生米阅读 380评论 0 0
  • ● 考研单词125个 ✔ ● 考研课程 ✔ ● 日语复习5,6,7 ✔ 我要吃橘子。 过...
    MickeyMinnie阅读 110评论 0 0