iOS 語音播報解決方案(實現(xiàn)支付寶/微信語音收款提示功能)

iOS10 語音播報填坑詳解(解決串行播報中斷問題)

在來聊這類需求的解決方案之前,咱們還是先來聊一聊這類需求的真實使用場景:語音播報间螟。語音播報需求運用最為廣泛的應該是收銀對賬了吴旋,就類似于支付寶、微信厢破、收錢吧等的收款語音提示一樣荣瑟。在iOS 10 之前,蘋果沒有提供通知擴展類的時候摩泪,如果想要實現(xiàn)殺進程也可以正常播報語音消息很難笆焰,從ios 10添加了這一個通知擴展類后,實現(xiàn)殺進程播報語音就相對簡單很多了见坑。

我們先來看一個陌生的Tagget

  • Notification Service Extension

這個Notification Service Extension 就是蘋果在 iOS 10的新系統(tǒng)中為我們添加的新特性嚷掠,這個新特性就能幫助我們用來解決殺死進程正常語音播報

原理流程圖

蘋果官方解釋:UNNotificationServiceExtension

詳細步驟

  • 創(chuàng)建一個通知擴展類
  • 添加語音播報邏輯代碼
  • 設置支持后臺播放
  • iOS10 以下系統(tǒng)如何實現(xiàn)串行播報

創(chuàng)建一個通知擴展類

  • 首先我點擊 Xcode 的 File -> New -> Target -> Notification Service Extension,新建一個通知擴展類Target鳄梅。
image
image

新建完后叠国,我們的工程會多出一個文件夾,這里示例Demo的Target命名為 NotificationSE戴尸,文件夾中有NotificationService.h NotificationService.m 文件粟焊,這兩個文件就是后面我們要用到的通知擴展類文件

image

在沒有對NotificationService做任何修改時,我們先來預覽下 .m 文件中都有哪些內(nèi)容

image

從上面的截圖孙蒙,我們可以看到项棠,.m 文件其實很簡單,就 2 個函數(shù)挎峦,其實后面我們對這個文件做邏輯處理香追,也是很簡單的。

添加語音播報邏輯代碼

  • 注意坦胶,這里我們使用的語音合成和播報組件也是蘋果官方提供的組件透典,AVSpeechSynthesizer晴楔,AVSpeechSynthesisVoiceAVSpeechUtterance

我們先來看下一段語音播放代碼片段:

    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];

現(xiàn)在我們將 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 內(nèi)容就是通知信息攜帶的數(shù)據(jù)凑队,后面我們?nèi)≌Z音播報的文案则果,通知欄的title,以及通知內(nèi)容都是從這個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];
}

// 新增語音播放代理函數(shù)叫惊,在語音播報完成的代理函數(shù)中款青,我們添加下面的一行代碼
- (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 文件中的每一個函數(shù)做下解釋

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

這個函數(shù)是通知擴展類的最為核心的函數(shù)了,你可以理解為這個就是接受到蘋果APNS 通知的一個鉤子函數(shù)赋访,每次當推送一條通知過來可都,都會執(zhí)行到這個函數(shù)體內(nèi),所以說我們的語音播報邏輯也是在這個鉤子函數(shù)中進行處理的蚓耽。

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

這個函數(shù)很簡單了渠牲,就是我們抽離出來的進行語音合成并播放出語音的函數(shù),我們傳遞一個語音文案作為此函數(shù)的參數(shù)即可步悠。

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

這個函數(shù)就是我們今天的主角了签杈,我們之所以能夠?qū)崿F(xiàn)當同時有多條通知同時推送,我們還能夠一條一條的串行逐條播放鼎兽,主要的功能就歸功到這個函數(shù)了答姥,這個函數(shù)是 AVSpeechSynthesizer 類的代理函數(shù),就是一段語音播放完成后執(zhí)行這個函數(shù)谚咬,每次當一條語音播放完成鹦付,都會被此函數(shù)勾住,我們在函數(shù)體內(nèi)實現(xiàn)我們的處理邏輯择卦。

  • - (void)serviceExtensionTimeWillExpire {}

此函數(shù)是擴展類自帶的一個函數(shù)敲长,從這個函數(shù)解釋我們可以看出,這個函數(shù)是當擴展被系統(tǒng)終止之前秉继,會調(diào)用到這個函數(shù)祈噪。

好了,.m文件的幾個關鍵的函數(shù)我們都做了相應的解釋了尚辑,可能還有些小伙伴不是很明白辑鲤,這些和解決通知串行逐一播報有什么關系尼,下面我就來根據(jù)自己的經(jīng)驗給大家做下詳細的解釋杠茬。

先來說下蘋果通知的通知欄問題

在蘋果通知中月褥,當來一條通知時弛随,我們的手機會叮一下,然后手機通知欄彈出通知宁赤。這里大家注意下撵幽,其實這個叮一下出來的通知欄也是有生命周期的。從通知欄被彈出來礁击,到通知欄最終被收起,其實中間蘋果給了限制時間逗载,大概就6秒左右的時長

說到6秒左右的時長哆窿,對于那些多條通知同時到達,需要串行來逐一播報厉斟,但是很多小伙伴們會遇到這樣一個問題:就是當同時來了多條通知挚躯,總是只能播報2-3條,然后就語音中斷了擦秽,后面的通知不會播報了码荔,遇到這些問題的小伙伴們有沒有注意到,其實只能播報2-3條感挥,這個時間差其實就是6秒左右缩搅,也就是通知欄的生命周期時長。

出現(xiàn)上面的問題的原因就是:當?shù)谝粭l通知來了触幼,彈出通知欄硼瓣,然后開始播報第一條語音,第一條播報完了置谦,開始播報第二天語音堂鲤,可能當?shù)诙煺Z音播報到一半了,但是這個時候媒峡,通知欄周期的時間到了瘟栖,這時通知欄就會收起,注意:谅阿,當通知欄收起時半哟,擴展類里面的代碼就會終止執(zhí)行,導致后面的語音播報終端奔穿。

上面說到當通知欄收起時镜沽,擴展類的代碼會終止執(zhí)行,這里又引出了另一個注意點:就是我們創(chuàng)建的這個擴展類也是有生命周期的贱田,并且這個生命周期和通知欄的生命周期他們是有依賴關系的缅茉。即:當通知欄收起時,擴展類就會被系統(tǒng)終止男摧,擴展內(nèi)里面的代碼也會終止執(zhí)行蔬墩,只有當下一個通知欄彈出來译打,擴展類就恢復功能

上面說到通知欄的出現(xiàn)和收起能夠影響到擴展類的功能,那我們是不是控制好通知欄的顯示和隱藏拇颅,就能解決多條串行問題尼奏司?

是的,我們只要控制好通知欄樟插,就可以解決上面的棘手問題韵洋,那么問題又來了,我們怎么才能控制通知欄的顯示和隱藏尼黄锤?感覺我們平時使用蘋果的推送搪缨,從來沒有關心過處理通知欄的顯示與隱藏,感覺從來沒有這樣用過鸵熟,是的副编,對應普通的需求,我們確實不需要關系通知欄顯示隱藏流强,感覺這些蘋果系統(tǒng)自己已經(jīng)處理好了痹届,通知來了就顯示通知欄,等5秒左右打月,周期結(jié)束就隱藏通知欄队腐。

其實啊,在擴展類里面中奏篙,蘋果已經(jīng)給我們指出了如何控制通知欄的顯示和隱藏香到,核心就是這行代碼:self.contentHandler(self.bestAttemptContent);,當我們調(diào)用到這行代碼报破,就是用來彈出通知欄的悠就,通知欄的隱藏不需要我們來控制了,因為5秒左右的生命周期結(jié)束后充易,它會自動隱藏梗脾。

是不是對這樣代碼既熟悉有陌生啊,熟悉是因為你的擴展類文件中確實有這行代碼盹靴,陌生是因為你之前從來都沒有用過這行代碼炸茧,不知道行代碼是用來干啥的。

好了稿静,既然self.contentHandler(self.bestAttemptContent); 這行核心代碼引用出來了梭冠,我們就回到最開始的問題,在沒有做任何處理時改备,為什么當同時來多條通知是控漠,語音播報就不能逐一播報尼,其實就是因為當每一條通知到達都會執(zhí)行這個函數(shù)- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {},有沒有發(fā)現(xiàn)盐捷,這個函數(shù)體里面 默認就是 執(zhí)行了 self.contentHandler(self.bestAttemptContent); 這行代碼偶翅。

假設 一次性同時來了 10條 通知,就會一次性調(diào)用了 10次 didReceiveNotificationRequest這個函數(shù)碉渡, 也就 執(zhí)行了 10次 self.contentHandler(self.bestAttemptContent)聚谁, 按照上面的說法,同時執(zhí)行10次滞诺,不就是同時彈出10次的 通知欄嗎形导,這里我調(diào)試時發(fā)現(xiàn),當同時來10條通知時习霹,通知欄并沒有同時彈出來10次骤宣,可能只彈出來1-2次。也就只能在這1-2次的時間長度中進行語音播報了序愚。

上面解釋這么多,那么我們到底該如何做尼等限,細心的同學發(fā)現(xiàn)了爸吮,我們上面 貼出來的 .m 代碼中,我們新增了一個 AVSpeechSynthesizer 類的代理函數(shù)望门,就是語音播報完成的函數(shù)形娇,我們將 呼出通知欄的代碼 self.contentHandler(self.bestAttemptContent); 添加到這個代理函數(shù)中。意思就是:當?shù)谝粭l語音播放完成了筹误,這時我們呼出通知欄顯示播放的內(nèi)容(通知欄的周期時間大概6秒左右)桐早,正好這時可以播放第二條語音,等第二條語音播放完成了厨剪,呼出第二個通知的通知欄哄酝,繼續(xù)播放第三天語音,以此類推祷膳。

看到這里陶衅,想必大家應該都理解了為啥之前總是語音播報中斷的問題。

還有一個很重要的函數(shù):- (void)serviceExtensionTimeWillExpire{}直晨,我們上面只是提了下搀军,具體他具體有什么功能尼?

我們發(fā)現(xiàn)serviceExtensionTimeWillExpire函數(shù)中,也調(diào)用了 self.contentHandler(self.bestAttemptContent) 這行代碼勇皇,它為啥也要調(diào)用這行代碼尼罩句?

這是因為:當我們在接受通知的鉤子函數(shù)中(didReceiveNotificationRequest)沒有調(diào)用self.contentHandler(self.bestAttemptContent) 這行代碼,這時就會出現(xiàn)一個現(xiàn)象:就是通知收到了敛摘,但是沒有通知欄出現(xiàn)门烂,這時蘋果就不允許了。蘋果規(guī)定兄淫,當一條通知達到后诅福,如果在30秒內(nèi)匾委,還沒有呼出通知欄,我就系統(tǒng)強制調(diào)用self.contentHandler(self.bestAttemptContent) 來呼出通知欄氓润。 這時想必大家都知道 serviceExtensionTimeWillExpire 函數(shù)的用途了吧

設置支持后臺播放

  • 配置應用支持后臺播放赂乐,這個只需要在Xcode中做下配置即可
image

這里需要注意:當勾上上面的配置后,可能會導致蘋果審核不通過咖气,這里我們可以在應用中添加一個語音播放的功能挨措,并錄制視頻告知蘋果用途,可能會過審崩溪。

iOS 10以下實現(xiàn)串行播報

核心代碼如下


// 監(jiān)聽通知函數(shù)中調(diào)用添加數(shù)據(jù)到隊列
- (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系統(tǒng)為 10及10 以上浅役,所以所 iOS10以下的系統(tǒng),是不支持使用通知擴展的
  • 通知擴展文件中是不支持斷點調(diào)試的伶唯,網(wǎng)上有說通過配置可以進行斷點觉既,可是我嘗試了 很多次,還是不能斷點乳幸,這里我的處理方式是瞪讼,通過使用 臨時的語音播報來代替斷點,在需要斷點的地方加一個語音播放粹断,如果播報出來了符欠,代表執(zhí)行了此行
  • 上面我們介紹了speechSynthesizer:didFinishSpeechUtterance 語音播放完成的代理函數(shù),可能有的小伙伴會遇到這個代理函數(shù)不執(zhí)行的情況瓶埋,這時我們需要將 AVSpeechSynthesizer 類的對象設置成全局屬性即可希柿。
  • iOS 10 以下的系統(tǒng),我們也想實現(xiàn)同時多條通知的串行播報該怎么實現(xiàn)尼养筒,我自己的做法是自己維護一個數(shù)組隊列曾撤,具體的實現(xiàn)參照下面代碼塊。
  • content-avilable 字段的值晕粪,需要配置為 1
  • 添加支持后天播放時盾戴,可能會被蘋果拒審
  • 如何實現(xiàn)擴展類和主工程之間的數(shù)據(jù)通信(這塊內(nèi)容會單獨的出一篇文章來介紹)
  • 待補充

示例Demo

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

總結(jié)

我們公司之前做的掃碼支付需求,支付成功后播報支付金額兵多,當時在開發(fā)這塊需求時尖啡,遇到了殺進程無法進行語音播報的問題,后面引入了iOS10 的通知擴展類來解決殺進程問題剩膘。在使用擴展類時衅斩,也是遇到了不少的問題和大坑,這里就逐一做了下總結(jié)怠褐,上面的講解也是填坑后的個人理解畏梆,如有錯誤之處,歡迎留言交流指出錯誤。

更多文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宪巨,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子溜畅,更是在濱河造成了極大的恐慌捏卓,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件慈格,死亡現(xiàn)場離奇詭異怠晴,居然都是意外死亡,警方通過查閱死者的電腦和手機浴捆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門蒜田,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人选泻,你說我怎么就攤上這事冲粤。” “怎么了页眯?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵梯捕,是天一觀的道長。 經(jīng)常有香客問我餐茵,道長,這世上最難降的妖魔是什么述吸? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任忿族,我火速辦了婚禮,結(jié)果婚禮上蝌矛,老公的妹妹穿的比我還像新娘道批。我一直安慰自己,他們只是感情好入撒,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布隆豹。 她就那樣靜靜地躺著,像睡著了一般茅逮。 火紅的嫁衣襯著肌膚如雪璃赡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天献雅,我揣著相機與錄音碉考,去河邊找鬼。 笑死挺身,一個胖子當著我的面吹牛侯谁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼墙贱,長吁一口氣:“原來是場噩夢啊……” “哼热芹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起惨撇,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤拗踢,失蹤者是張志新(化名)和其女友劉穎鲸沮,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡椒拗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了叨恨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片识藤。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖祷蝌,靈堂內(nèi)的尸體忽然破棺而出茅撞,到底是詐尸還是另有隱情,我是刑警寧澤巨朦,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布米丘,位于F島的核電站,受9級特大地震影響糊啡,放射性物質(zhì)發(fā)生泄漏拄查。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一棚蓄、第九天 我趴在偏房一處隱蔽的房頂上張望堕扶。 院中可真熱鬧,春花似錦梭依、人聲如沸稍算。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽糊探。三九已至,卻和暖如春河闰,著一層夾襖步出監(jiān)牢的瞬間科平,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工姜性, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留匠抗,地道東北人。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓污抬,卻偏偏與公主長得像汞贸,于是被迫代替她去往敵國和親绳军。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 1矢腻、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 15,980評論 3 119
  • 昆明梁艷分享129天门驾。網(wǎng)課初級五期。2017.09.23 考慮了很久多柑,都沒有定下到底分享什么奶是,主要是今...
    詩心小鹿閱讀 212評論 0 1
  • 今年的初九別樣冷清。二姐三姐回去上班了竣灌,姐姐姐夫早上倒是在家聂沙,但是下午就要走了,因為姐夫明天上班初嘹。 爸爸聽見煙花的...
    辰方乙羲閱讀 419評論 0 1
  • 杜月笙的大名如雷貫耳及汉,早些時候辦公室同事也提到此人,一直想閱讀有關他的書籍屯烦。遺憾的是坷随,當時在豆瓣里搜索了有關此人書...
    笨笨花生米閱讀 397評論 0 0
  • ● 考研單詞125個 ? ● 考研課程 ? ● 日語復習5,6,7 ? 我要吃橘子。 過...
    MickeyMinnie閱讀 124評論 0 0