1. 程式人生 > >iOS 語音播報解決方案(實現支付寶語音收款功能)

iOS 語音播報解決方案(實現支付寶語音收款功能)

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

在來聊這類需求的解決方案之前,咱們還是先來聊一聊這類需求的真實使用場景:語音播報。語音播報需求運用最為廣泛的應該是收銀對賬了,就類似於支付寶、微信、收錢吧等的收款語音提示一樣。在iOS 10 之前,蘋果沒有提供通知擴充套件類的時候,如果想要實現殺程序也可以正常播報語音訊息很難,從iOS 10添加了這一個通知擴充套件類後,實現殺程序播報語音就相對簡單很多了。

我們先來看一個陌生的Tagget

  • Notification Service Extension

這個Notification Service Extension 就是蘋果在 iOS 10的新系統中為我們新增的新特性,這個新特性就能幫助我們用來解決殺死程序正常語音播報
在這裡插入圖片描述

詳細步驟

  • 建立一個通知擴充套件類
  • 新增語音播報邏輯程式碼
  • 設定支援後臺播放
  • iOS 10 以下系統如何實現序列播報

建立一個通知擴充套件類

首先我點選 Xcode 的 File -> New -> Target -> Notification Service Extension,新建一個通知擴充套件類Target。
在這裡插入圖片描述
在這裡插入圖片描述

新建完後,我們的工程會多出一個資料夾,這裡示例Demo的Target命名為 NotificationSE,資料夾中有NotificationService.h NotificationService.m 檔案,這兩個檔案就是後面我們要用到的通知擴充套件類檔案
在這裡插入圖片描述

在沒有對NotificationService做任何修改時,我們先來預覽下 .m 檔案中都有哪些內容
在這裡插入圖片描述

從上面的截圖,我們可以看到,.m 檔案其實很簡單,就 2 個函式,其實後面我們對這個檔案做邏輯處理,也是很簡單的。

新增語音播報邏輯程式碼

注意,這裡我們使用的語音合成和播報元件也是蘋果官方提供的元件,AVSpeechSynthesizer,AVSpeechSynthesisVoice,AVSpeechUtterance
我們先來看下一段語音播放程式碼片段:

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 檔案做修改,使之支援語音播報。並且能支援多條通知同時過來的序列播報。完整檔案如下:

#import "NotificationService.h"
#import 
#import 

@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中做下配置即可
    在這裡插入圖片描述

這裡需要注意:當勾上上面的配置後,可能會導致蘋果稽核不通過,這裡我們可以在應用中新增一個語音播放的功能,並錄製視訊告知蘋果用途,可能會過審。

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
  • 新增支援後天播放時,可能會被蘋果拒審
  • 如何實現擴充套件類和主工程之間的資料通訊(這塊內容會單獨的出一篇文章來介紹)
  • 待補充

總結

我們公司之前做的掃碼支付需求,支付成功後播報支付金額,當時在開發這塊需求時,遇到了殺程序無法進行語音播報的問題,後面引入了iOS10 的通知擴充套件類來解決殺程序問題。在使用擴充套件類時,也是遇到了不少的問題和大坑,這裡就逐一做了下總結,上面的講解也是填坑後的個人理解,如有錯誤之處,歡迎留言交流指出錯誤。