簡介
推送基本上是每一個APP必備的功能,而iOS 10新增了UserNotificationKit框架,整合了之前的通知,而且新增了很多特性。
1.通知內(nèi)容更加豐富
- 由之前的alert到現(xiàn)在的title,subTitle,body。
- 為推送增加了附件,包括符合格式和大小的圖片、音頻和視頻。
2.方便對推送的周期進行管理
- 更新推送
- 刪除推送
- 查看推送
新框架
#ifdef NSFoundationVersionNumber_iOS_9_x_Max
#import <UserNotifications/UserNotifications.h>
#endif
通過UNUserNotificationCenter來管理本地和遠程通知。
1.首先打開推送開關(guān)

2. 獲取權(quán)限
我們需要在 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions注冊通知,代碼如下
UNUserNotificationCenter *notifiCenter = [UNUserNotificationCenter currentNotificationCenter];
UNAuthorizationOptions options = UNAuthorizationOptionNone | UNAuthorizationOptionBadge| UNAuthorizationOptionSound | UNAuthorizationOptionAlert;
[notifiCenter requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError * _Nullable error) {
}];
[[UIApplication sharedApplication] registerForRemoteNotifications];
3. 注冊APNS,獲取deviceToken
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
[JPUSHService registerDeviceToken:deviceToken];
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
NSLog(@"did Fail To Register For Remote Notifications With Error: %@", error);
}
4. iOS7以后如果想要在后臺做一些操作
- 需要在APNS增加字段
"content-available":1,"mutable-content":1 -
需要在Background Modes中增加Remote notifications
工程推送配置
5. 收到推送調(diào)用的方法
- 這是應(yīng)用處于前臺時 收到推送觸發(fā)
- (void)jpushNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(NSInteger))completionHandler {
NSDictionary * userInfo = notification.request.content.userInfo;
UNNotificationRequest *request = notification.request; // 收到推送的請求
UNNotificationContent *content = request.content; // 收到推送的消息內(nèi)容
NSNumber *badge = content.badge; // 推送消息的角標
NSString *body = content.body; // 推送消息體
UNNotificationSound *sound = content.sound; // 推送消息的聲音
NSString *subtitle = content.subtitle; // 推送消息的副標題
NSString *title = content.title; // 推送消息的標題
if([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
[JPUSHService handleRemoteNotification:userInfo];
NSLog(@"iOS10 前臺收到遠程通知");
}
else {
// 判斷為本地通知
NSLog(@"iOS10 前臺收到本地通知:{\nbody:%@,\ntitle:%@,\nsubtitle:%@,\nbadge:%@,\nsound:%@,\nuserInfo:%@\n}",body,title,subtitle,badge,sound,userInfo);
}
completionHandler(UNNotificationPresentationOptionAlert); // 需要執(zhí)行這個方法,選擇是否提醒用戶,有Badge、Sound、Alert三種類型可以設(shè)置
}
操作的回調(diào)方法:不管應(yīng)用在前臺、后臺還是被手動劃掉,下面三種情況將觸發(fā)該方法。
- 點擊通知進入應(yīng)用
2.點擊action
- 清除了category是UNNotificationCategoryOptionCustomDismissAction的通知
- (void)jpushNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler {
NSDictionary * userInfo = response.notification.request.content.userInfo;
UNNotificationRequest *request = response.notification.request; // 收到推送的請求
UNNotificationContent *content = request.content; // 收到推送的消息內(nèi)容
NSNumber *badge = content.badge; // 推送消息的角標
NSString *body = content.body; // 推送消息體
UNNotificationSound *sound = content.sound; // 推送消息的聲音
NSString *subtitle = content.subtitle; // 推送消息的副標題
NSString *title = content.title; // 推送消息的標題
if([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
[JPUSHService handleRemoteNotification:userInfo];
NSLog(@"iOS10 收到遠程通知");
}
else {
// 判斷為本地通知
NSLog(@"iOS10 收到本地通知:{\nbody:%@,\ntitle:%@,\nsubtitle:%@,\nbadge:%@,\nsound:%@,\nuserInfo:%@\n}",body,title,subtitle,badge,sound,userInfo);
}
completionHandler(); // 系統(tǒng)要求執(zhí)行這個方法
}
收到遠程推送的回調(diào)方法:APNS帶有"content-available":1字段,并且應(yīng)用在前臺或者后臺時收到遠程推送,將觸發(fā)該方法。(注意:應(yīng)用被手動劃掉將無法觸發(fā))
可以在這個方法里做一些后臺操作(下載數(shù)據(jù),更新UI等),記得修改Background Modes。
- (void)JPush_application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
[JPUSHService handleRemoteNotification:userInfo];
completionHandler(UIBackgroundFetchResultNewData);
}
UNNotificationContentExtension - 通知內(nèi)容擴展
通知內(nèi)容擴展需要新建一個 UNNotificationContentExtension Target,之后只需在 viewcontroller 的中實現(xiàn)相應(yīng)的接口,即可以對 app 的通知頁面進行自定義擴展,擴展主要用于自定義 UI。
UNNotificationServiceExtension - 通知服務(wù)擴展

UNNotificationServiceExtension 提供在遠程推送將要被 push 出來前,處理推送顯示內(nèi)容的機會。此時可以對通知的 request.content 進行內(nèi)容添加,如添加附件,userInfo 等。
使用UNNotificationServiceExtension,你有30秒的時間處理這個通知,可以同步下載圖像和視頻到本地,然后包裝為一個UNNotificationAttachment扔給通知,這樣就能展示用服務(wù)器獲取的圖像或者視頻了。這里需要注意:如果數(shù)據(jù)處理失敗,超時,extension會報一個崩潰信息,但是通知會用默認的形式展示出來,app不會崩潰。
新建通知擴展
Xcode File ->New ->Target

然后寫名字,下一步,就可以了
此時我們的目錄結(jié)構(gòu)里面,已經(jīng)多出了一個文件夾了



注意看上圖,這里的bundleID是你的工程名字的bundleID加上通知擴展的名稱。
不要修改,系統(tǒng)創(chuàng)建的時候就創(chuàng)建好了,不過我還是給大家說一下這個格式
如果你的工程的BundleID是comTaoShengyijiu.pushDemo,則這個擴展的BundleID就是comTaoShengyijiu.ZYBaseTestPushExtend, 最后的后綴是看咱們創(chuàng)建服務(wù)擴展時候的名字。其他的小細節(jié),大家可以看看。
到這一步,我們就新建了一個服務(wù)通知類的擴展。
因為我們公司的APP是一款類似于支付寶的理財工具,產(chǎn)品的需求就是類似于支付寶收錢吧一類,只要APP收到一筆款,就要能實時播報出來,無論程序處于前臺、后臺還是殺死的情況下都要能正常播報。所以我在通知擴展中就是啟動系統(tǒng)自帶的音頻服務(wù)讀出推送內(nèi)容。
普及知識時間
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler
這個函數(shù)是通知擴展類的最為核心的函數(shù)了,你可以理解為這個就是接受到蘋果APNS 通知的一個鉤子函數(shù),每次當推送一條通知過來,都會執(zhí)行到這個函數(shù)體內(nèi),所以說我們的語音播報邏輯也是在這個鉤子函數(shù)中進行處理的。
先來說下蘋果通知的通知欄問題
在蘋果通知中,當來一條通知時,我們的手機會叮一下,然后手機通知欄彈出通知。這里大家注意下,其實這個叮一下出來的通知欄也是有生命周期的。從通知欄被彈出來,到通知欄最終被收起,其實中間蘋果給了限制時間,大概就6秒左右的時長(注意,如果你要播報的內(nèi)容超過6秒,你就要去控制什么時候彈出通知欄了,要不然會出現(xiàn)語音無法全部播報出來的情況)。
說到6秒左右的時長,對于那些多條通知同時到達,需要串行來逐一播報,但是很多小伙伴們會遇到這樣一個問題:就是當同時來了多條通知,總是只能播報2-3條,然后就語音中斷了,后面的通知不會播報了,遇到這些問題的小伙伴們有沒有注意到,其實只能播報2-3條,這個時間差其實就是6秒左右,也就是通知欄的生命周期時長。
出現(xiàn)上面的問題的原因就是:當?shù)谝粭l通知來了,彈出通知欄,然后開始播報第一條語音,第一條播報完了,開始播報第二條語音,可能當?shù)诙l語音播報到一半了,但是這個時候,通知欄周期的時間到了,這時通知欄就會收起,注意:當通知欄收起時,擴展類里面的代碼就會終止執(zhí)行,導致后面的語音播報終端。
上面說到當通知欄收起時,擴展類的代碼會終止執(zhí)行,這里又引出了另一個注意點:就是我們創(chuàng)建的這個擴展類也是有生命周期的,并且這個生命周期和通知欄的生命周期他們是有依賴關(guān)系的。即:當通知欄收起時,擴展類就會被系統(tǒng)終止,擴展內(nèi)里面的代碼也會終止執(zhí)行,只有當下一個通知欄彈出來,擴展類就恢復(fù)功能
上面說到通知欄的出現(xiàn)和收起能夠影響到擴展類的功能,那我們是不是控制好通知欄的顯示和隱藏,就能解決多條串行問題呢?
是的,我們只要控制好通知欄,就可以解決上面的棘手問題,那么問題又來了,我們怎么才能控制通知欄的顯示和隱藏呢?感覺我們平時使用蘋果的推送,從來沒有關(guān)心過處理通知欄的顯示與隱藏,感覺從來沒有這樣用過,是的,對應(yīng)普通的需求,我們確實不需要關(guā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)這行代碼。
假設(shè)一次性同時來了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ù)播放第三天語音,以此類推。
看到這里,想必大家應(yīng)該都理解了為啥之前總是語音播報中斷的問題。
還有一個很重要的函數(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ù)的用途了吧。
小編自己Demo里的代碼來了
#import "NotificationService.h"
#import <AVFoundation/AVFoundation.h>
@interface NotificationService ()<AVSpeechSynthesizerDelegate>
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
/** 語音合成引擎 */
@property (nonatomic, strong) AVSpeechSynthesizer *voiceSpeaker;
/** 彈框是否已經(jīng)展示 */
@property (nonatomic, assign) BOOL alertIsDisplayed;
@end
PS:這里通過判斷文字個數(shù)來控制通知欄什么時候顯示
/** 文字限制長度 暫定為15字*/
static int contentLengthLimit = 18;
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
NSError *activeErr = nil;
NSError *cateroyErr = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&cateroyErr];
[[AVAudioSession sharedInstance] setActive:YES error:&activeErr];
self.alertIsDisplayed = NO;
// Modify the notification content here...
self.bestAttemptContent.title = [NSString stringWithFormat:@"%@By濤聲", self.bestAttemptContent.title];
[self speakStringFromServer:self.bestAttemptContent.body];
}
#pragma mark - 處理并播放服務(wù)器返回的內(nèi)容
- (void)speakStringFromServer:(NSString *)string {
if (string.length == 0) {
// 如果字符串長度為0,則直接彈出通知Alert,不執(zhí)行任何操作 通知欄的隱藏不需要我們來控制,因為5秒左右的生命周期結(jié)束后,它會自動隱藏
self.alertIsDisplayed = YES;
self.contentHandler(self.bestAttemptContent);
return;
}
// 如果文字過長的話,會導致文字播放到一半時出現(xiàn)通知的聲音,故將通知聲音關(guān)閉
self.bestAttemptContent.sound = nil;
if (string.length <= contentLengthLimit) {
// 如果文字長度較短的話則直接彈出通知欄并且開啟通知聲音
self.bestAttemptContent.sound = [UNNotificationSound defaultSound];
// 如果需要播放的內(nèi)容長度小于限制長度,5秒時間足以播放完畢,則直接彈出Alert。
self.alertIsDisplayed = YES;
self.contentHandler(self.bestAttemptContent);
}
NSString *needStr = [self getNumberFromString:string];
[needStr stringByAppendingString:@""];
NSString *tempStr = [NSString stringWithFormat:@",%@", needStr];
NSString *finalStr = [string stringByReplacingOccurrencesOfString:needStr withString:tempStr];
[self speakString:finalStr];
}
- (void)speakString:(NSString *)string {
if (self.voiceSpeaker) {
AVSpeechUtterance *aUtterance = [AVSpeechUtterance speechUtteranceWithString:string];
[aUtterance setVoice:[AVSpeechSynthesisVoice voiceWithLanguage:@"zh-TW"]];
aUtterance.rate = 0.5; //設(shè)置語速
aUtterance.volume = 1; //設(shè)置音量(0.0~1.0)默認為1.0
aUtterance.pitchMultiplier = 1; //設(shè)置語調(diào) (0.5-2.0)
[self.voiceSpeaker speakUtterance:aUtterance];
}
}
#pragma mark - 獲取字符串中的數(shù)字 以便在數(shù)值處特意停頓一下
- (NSString *)getNumberFromString:(NSString *)string {
NSScanner *scanner = [NSScanner scannerWithString:string];
[scanner scanUpToCharactersFromSet:[NSCharacterSet decimalDigitCharacterSet] intoString:nil];
float num ;
[scanner scanFloat:&num];
NSString *tempStr = [NSString stringWithFormat:@"%.2f", num];
NSDecimalNumber *resultNum = [NSDecimalNumber decimalNumberWithString:tempStr];
NSString *resultStr = [NSString stringWithFormat:@"%@", resultNum];
return resultStr;
}
#pragma mark - AVSpeechSynthesizerDelegate
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didStartSpeechUtterance:(AVSpeechUtterance *)utterance
{
NSLog(@"開始");
}
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance
{
NSLog(@"結(jié)束");
}
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didPauseSpeechUtterance:(AVSpeechUtterance *)utterance
{
NSLog(@"暫停");
}
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didContinueSpeechUtterance:(AVSpeechUtterance *)utterance
{
NSLog(@"繼續(xù)");
}
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didCancelSpeechUtterance:(AVSpeechUtterance *)utterance
{
NSLog(@"取消");
self.alertIsDisplayed = YES;
self.contentHandler(self.bestAttemptContent);
}
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer willSpeakRangeOfSpeechString:(NSRange)characterRange utterance:(AVSpeechUtterance *)utterance {
if (self.alertIsDisplayed == NO) {
if (characterRange.location >= utterance.speechString.length / 4) {
// 如果文字長度大于限制,則可能通知欄彈出5秒內(nèi)無法播放完畢,則暫定為播放到1/4時再彈出狀態(tài)欄
self.alertIsDisplayed = YES;
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.
// 當一條通知達到后,如果在30秒內(nèi),還沒有呼出通知欄,系統(tǒng)就強制調(diào)用來呼出通知欄
self.alertIsDisplayed = YES;
self.contentHandler(self.bestAttemptContent);
}
#pragma mark - 懶加載
- (AVSpeechSynthesizer *)voiceSpeaker {
if (!_voiceSpeaker) {
_voiceSpeaker = [[AVSpeechSynthesizer alloc] init];
_voiceSpeaker.delegate = self;
}
return _voiceSpeaker;
}
可能遇見的一些問題
-
Q.調(diào)試這個通知擴展類,為什么我跑程序的時候,打斷點無反應(yīng)?
A. 因為你選擇的這是因為你跑的target不對,正確的做法是,跑正確的 target,具體如下圖
1523432182686.jpg

這個時候再打斷點調(diào)試,就OK了。

