原文
在iOS10上,蘋果將原來散落在UIKit中各處的用戶通知相關的代碼進行重構,剝離,打造了一個全新的通知框架-UserNotifications。筆者最近在開發公司通知相關的需求,跟著WWDC2016的視頻和官方文檔,學習了一下新框架。同時,在學習過程中,和老框架對應Api進行對比,有了個人的感受和看法。
首先,對於通知框架,其框架功能包括以下四類
申請權限/注冊配置
發送本地通知
展示和響應本地/遠程通知
App Extension
在UserNotifications框架中,最核心的類是UNUserNotificationCenter,這個類的是這三項功能的管理類,通過注入到currentNotificationCenter進行對消息的管理。而在之前,大部分操作的管理者UIApplication的單例。
1.申請權限/注冊配置
在新框架中,將申請權限和注冊配置拆分為兩個Api,使得職責更加分明。先看舊框架的實現。
UIUserNotificationType types = ...//通知可顯示的樣式 UIUserNotificationSettings *settting = [UIUserNotificationSettings settingsForTypes:types categories:nil]; //將樣式和Category一起生成配置 [[UIApplication sharedApplication] registerUserNotificationSettings:settting];//用這個配置注冊
Category的概念,是一個通知的種類。對同種類的通知,可以規定對應的一組按鈕操作,同時,iOS10以上的Rich Notification(可以展示圖片,自定義視圖)的區分讀也是通過category來區分。
而在老的通知框架中,上面第二行代碼中可以看到,category和展示樣式一同綁定起來用於申請權限。實際上,Category的概念用於區分接收消息的種類,也就是其實這個概念屬於接收消息後進行自定義處理的使用者,也就是App內部開發。而對於申請權限,其實最終是向App外部的交互,也就是App用戶關心的。而新框架進行拆分後,就更加自由和職責區分了。
UserNotifications中請求權限的用法
[[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:... completionHandler:^(BOOL granted, NSError * _Nullable error) { if (granted) { //...如果被授權了 } }];
同時,增加一個callback將授權後的狀態返回,就像分散型網絡請求一般。
對於遠程通知,還需要一個獲取用戶token的操作,並使用這個token進行APNs推送。但是奇怪的是,對於申請token這個操作,新框架中卻沒有對應的Api,沿用舊的Api。
[[UIApplication sharedApplication] registerForRemoteNotifications]; //AppDelegate - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{ .... }
獲取操作被放在AppDelegate的回調中,如果按照新框架的設計,應該會被設計成一個帶callBack的Api。所以筆者猜測沿用舊設計的原因是,在以後,獲取token的操作不一定由發起請求後產生,也許可能會有多種情況下產生獲取操作,也可能token會過期可以進行更新,重置操作。所以delegate的集中式管理更加擁有拓展性,當然也有可能這個Api忘記遷移哈哈。
配置Category並注冊
對於Category的作用,上文已經有介紹,而Category中目前主要的功能,是對應一組按鈕操作。
Category的使用分四步
創建Category,設置identifer,配置一組按鈕(可無)
將Category注冊到UIApplication(舊)/UNNotificationCenter(新)中
發送本地或遠程消息時需要按鈕或自定義視圖時帶上對應的categoryIdentifer
收到消息進行響應時通過消息中攜帶的categoryIdentifer進行分類,可以指定不同的操作
舊框架中按鈕動作對象UIUserNotificationAction和新的UNNotificationAction差異性不大,主要關心的是
按鈕上顯示的文字
按鈕的identifer,用於區分按鈕
是否需要跳轉到前台,喚起主App
而差異性在於
舊框架分為可變對象和不可變對象,新框架只有不可變對象加上實例化方法
新框架中,對於可輸入操作的按鈕,拆分成一個子類,可以使輸入操作自定義性和拓展性更強
新建一個按鈕也很簡單
UNNotificationAction *callDriverAction = [UNNotificationAction actionWithIdentifier:@"xxx" title:@"呼叫司機" options:UNNotificationActionOptionForeground];
options中可以選擇是否需要解鎖後才能操作、按鈕顏色是否為紅色(代表操作有破壞性)、是否打開App。
然後將整組按鈕加入UNNotificationCategory(新)或UIUserNotificationCategory(舊)中。
這兩個對象差異性在於
舊框架分為可變對象和不可變對象,新框架只有不可變對象加上實例化方法
舊框架按鈕有兩種使用環境,所以需要設置UIUserNotificationActionContext
新框架支持配置按鈕的響應是否發送到UNNotificationCenter的Delegate或CarPlay中
新框架支持Intent框架(SiriKit使用到)
UIUserNotificationActionContext
UIUserNotificationActionContextDefault-對應iOS10以下的的“提醒”樣式,是一個彈框
UIUserNotificationActionContextMinimal-對應iOS10以下的“橫幅”樣式,最多支持兩個按鈕橫向並排,多於兩個按鈕會取前兩個
為什麼這個在新的框架中被去掉了呢?
iOS10上,提醒樣式不再是一個彈框,而是和“橫幅”統一,只是不會主動往上收起
iOS10上,橫幅的按鈕不再是橫向並排,而是豎著排放,也不會限制個數
之後調用setNotificationCategories方法注冊即可
[[UNUserNotificationCenter currentNotificationCenter]setNotificationCategories:[NSSet setWithObject:self.category]];
在運行app時,原來的set裡的categories並不會清空,所以需要將整個Set傳進去作為參數,這樣會把原來的set完整替換成新的set。每次調用時,整個set替換原來的set。
同時,對於通知設置和Category設置,拆分了兩個Api去獲取當前的設置。
- (void)getNotificationCategoriesWithCompletionHandler:(void(^)(NSSet *categories))completionHandler; - (void)getNotificationSettingsWithCompletionHandler:(void(^)(UNNotificationSettings *settings))completionHandler;
而對於按鈕的操作響應,將在第三部分響應通知中介紹。
2.發送本地通知
在iOS10以前,本地通知的類用的是UILocalNotification。而在UserNotifications框架中,將本地通知和遠程通知統一起來,然後將通知拆分成request=content(內容)+trigger(觸發器)的模式,十分像網絡請求的思路。這樣設計的好處是對於遠程通知和本地通知對於響應的處理得到統一,同時也不會像以前一樣,將本地特有的功能隨意堆砌在本地通知類中,而是通過差異化配置類去進行注入。
UNNotificationContent
新的通知內容類,有可變子類。同時有以下新功能
支持一個UNNotificationAttachment(附件)數組,附件用一個identifer+文件路徑構成,可攜帶視頻/圖片等,而這些內容也為iOS10的RichNotification自定義視圖提供了素材
支持Title+Subtitle,Apns對應字段也同步支持
通知聲音有了特定類UNNotificationSound進行管理,以後拓展性,自由度更好了
threadIdentifer(主要用於Extesion Content中,在最後一節App Extension中會介紹)
UNNotificationTrigger
而之前散落成一個個不同類型的觸發相關屬性,也被匯總成了UNNotification的子類。有以下幾個類
UNPushNotificationTrigger-這個類代表這條消息是由APNs推送過來的,也就是這個trigger是否是這個類是區分本地通知和遠程通知的標志,對於做多系統b版本兼容時,很有幫助
UNTimeIntervalNotificationTrigger-根據相隔時間觸發,就和計時器一樣,注意timeInterval要大於0,且希望repeats的話,需要timeInterval大於60
UNCalendarNotificationTrigger-根據日歷時間觸發
UNLocationNotificationTrigger-根據定位在某個位置觸發
UNNotificationRequest
request除了包含content和trigger外,還有一個非常主要的屬性,那就是identifer。
有了Identifer,可以實現通知的更新和移除。
把通知分成待展示(已投遞,但未觸發或未展示)和已經展示過進入用戶通知中心的兩種。遠程通知在後台收到會立即展示。也就是一共有四種情況
更新未展示的通知(本地&遠程前台)
更新已展示的通知(本地&遠程)
取消未展示的通知(本地)
取消已展示的通知(本地)
而標識同一通知的標志,就是通知的identifer。
而發送/更新通知用的是同一個Api,就是將request添加到UNNotificationCenter裡,同時也有一個callback回調狀態
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:[UNNotificationRequest requestWithIdentifier:sameIdentifer content:content trigger:trigger] withCompletionHandler:^(NSError * _Nullable error) { ... }];
而取消或移除未展示和已展示的通知,分開了兩組Api方便根據情況選擇,同時也有get方法獲取當前已展示/未展示的隊列裡的所有通知,而且和添加/更新不同,移除只需要一個identifer,且可以同時傳入一組identifer,一次性移除多個
- (void)getPendingNotificationRequestsWithCompletionHandler:(void(^)(NSArray *requests))completionHandler;//獲取未展示通知 - (void)removePendingNotificationRequestsWithIdentifiers:(NSArray *)identifiers;//取消未展示通知 - (void)removeAllPendingNotificationRequests;//取消所有未展示通知隊列裡的通知 1 - (void)getDeliveredNotificationsWithCompletionHandler:(void(^)(NSArray *notifications))completionHandler;//獲取已展示通知 - (void)removeDeliveredNotificationsWithIdentifiers:(NSArray *)identifiers;//從通知中心中移除已展示通知 - (void)removeAllDeliveredNotifications;//移除所有通知中心裡的通知
3.展示和響應
在舊框架裡面,這部分Api是最為混亂的部分。一共有1..2.....7個delegate......而且之中有的還有取代關系,也就是有其中一個另一個不執行......
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo;//前台收到遠程通知 - (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification;//前台收到本地通知 - (void)application:(UIApplication *)application handleActionWithIdentifier:(nullable NSString *)identifier forLocalNotification:(UILocalNotification *)notification completionHandler:(void(^)())completionHandler;//iOS9之前本地通知點擊按鈕後 - (void)application:(UIApplication *)application handleActionWithIdentifier:(nullable NSString *)identifier forRemoteNotification:(NSDictionary *)userInfo withResponseInfo:(NSDictionary *)responseInfo completionHandler:(void(^)())completionHandler;//iOS9遠程通知點擊按鈕後 - (void)application:(UIApplication *)application handleActionWithIdentifier:(nullable NSString *)identifier forRemoteNotification:(NSDictionary *)userInfo completionHandler:(void(^)())completionHandler;//iOS9之前遠程通知點擊按鈕後 - (void)application:(UIApplication *)application handleActionWithIdentifier:(nullable NSString *)identifier forLocalNotification:(UILocalNotification *)notification withResponseInfo:(NSDictionary *)responseInfo completionHandler:(void(^)())completionHandler;//iOS9本地通知點擊按鈕 - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler;//iOS7以上收到遠程通知調用(未被廢棄)
這麼亂,讓我重新分類。
收到通知
點擊通知本身
點擊通知的自定義按鈕
收到通知
在觸發本地通知時,App並不會被喚醒,所以本地只有前台時才有回調。
本地+前台-didReceiveLocalNotification
遠程+前台-didReceiveRemoteNotification和didReceiveRemoteNotification:fetchCompletionHandler若有後面則只執行後面那個
遠程+後台-喚醒並執行didReceiveRemoteNotification:fetchCompletionHandler
點擊通知本身
本地+App存活-didReceiveLocalNotification
本地+App未存活--(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions,其中lanchOptions中UIApplicationLaunchOptionsLocalNotificationKey為UILocalNotification對象
遠程+App存活-didReceiveRemoteNotification和didReceiveRemoteNotification:fetchCompletionHandler若有後面則只執行後面那個
遠程+App未存活-didReceiveRemoteNotification:fetchCompletionHandler有則執行,無則-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions,其中lanchOptions中UIApplicationLaunchOptionsRemoteNotificationKey
為通知的userInfo
點擊自定義按鈕
iOS9handleActionWithIdentifier,有responseInfo後綴的優先調,無則調用無後綴的
iOS8調用無後綴的
整理完感覺就是,亂,還涉及到lauchOptions等無關api,api之間還會覆蓋,還有一些不是收到和響應都會調。
而新框架單純拆分為收到消息+點擊事件響應,且不再在Api層面區分遠程與本地通知,使對待通知的路徑變得統一。
通知的收到
在前台的時候,收到不管是本地還是遠程通知都會被新的delegate接管。以前的通知框架,在前台收到通知時,默認是不展示(沒有橫幅等)的。而新delegate可以在前台收到通知後,展示前,做一些處理,包括以什麼形式展示,已經做一些自定義操作。而這個Delegate只有App在前台才會執行。因為在後台App收到通知時,通知不會喚醒App(不是喚起),而App可能是被殺死的,所以這個Delegate沒有被賦值,所以統一只在App在前台時才執行。而將新的Api對應舊的Api轉換起來的話,就是下面的代碼。
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler{ BOOL isRemote = NO; if ([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { isRemote = YES; } UILocalNotification *localNotification;//從新的轉換為舊的本地通知 if (!isRemote && [[UIApplication sharedApplication].delegate respondsToSelector:@selector(application:didReceiveLocalNotification:)]) { [[UIApplication sharedApplication].delegate application:[UIApplication sharedApplication] didReceiveLocalNotification:localNotification]; } else if (isRemote) { if ([[UIApplication sharedApplication].delegate respondsToSelector:@selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)]) { [[UIApplication sharedApplication].delegate application:[UIApplication sharedApplication] didReceiveRemoteNotification:notification.request.content.userInfo fetchCompletionHandler:^(UIBackgroundFetchResult result) {}]; } else if ([[UIApplication sharedApplication].delegate respondsToSelector:@selector(application:didReceiveRemoteNotification:)]){ [[UIApplication sharedApplication].delegate application:[UIApplication sharedApplication] didReceiveRemoteNotification:notification.request.content.userInfo]; } } completionHandler(UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionAlert);//決定前台展示形式 }
而CompletionHandler裡傳入希望通知支持的類型(橫幅、紅點、聲音),在自定義操作完成後執行這個block就可。當然可以在這裡為不同消息定制不同的支持類型。
點擊通知或按鈕的操作響應
新框架中,點擊操作和收到通知的Api的響應終於被區分開了。而且點擊操作也包含點擊通知本身,以及有了點清除關閉通知的事件響應。拆分點擊和收到的Api是很好的,因為點擊通知和按鈕時,實際上會將app喚起到前台,一般這時候需要進行頁面跳轉,界面狀態恢復等等,這些在收到通知時是沒有必要的。
新的Api和就的轉換的話就是下面這樣
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler{ BOOL isRemote = NO; if ([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { isRemote = YES; } UILocalNotification *localNotification;//從新的轉換為舊的本地通知 if ([response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier] || [response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) { if (!isRemote && [[UIApplication sharedApplication].delegate respondsToSelector:@selector(application:didReceiveLocalNotification:)]) { [[UIApplication sharedApplication].delegate application:[UIApplication sharedApplication] didReceiveLocalNotification:localNotification]; } else if (isRemote) { if ([[UIApplication sharedApplication].delegate respondsToSelector:@selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)]) { [[UIApplication sharedApplication].delegate application:[UIApplication sharedApplication] didReceiveRemoteNotification:response.notification.request.content.userInfo fetchCompletionHandler:^(UIBackgroundFetchResult result) {}]; } else if ([[UIApplication sharedApplication].delegate respondsToSelector:@selector(application:didReceiveRemoteNotification:)]){ [[UIApplication sharedApplication].delegate application:[UIApplication sharedApplication] didReceiveRemoteNotification:response.notification.request.content.userInfo]; } } } else{ if (!isRemote) { if ([[UIApplication sharedApplication].delegate respondsToSelector:@selector(application:handleActionWithIdentifier:forLocalNotification:withResponseInfo:completionHandler:)]) { [[UIApplication sharedApplication].delegate application:[UIApplication sharedApplication] handleActionWithIdentifier:response.actionIdentifier forLocalNotification:localNotification withResponseInfo:@{} completionHandler:^{}]; } else if ([[UIApplication sharedApplication].delegate respondsToSelector:@selector(application:handleActionWithIdentifier:forLocalNotification:completionHandler:)]){ [[UIApplication sharedApplication].delegate application:[UIApplication sharedApplication] handleActionWithIdentifier:response.actionIdentifier forLocalNotification:localNotification completionHandler:^{}]; } } else{ if ([[UIApplication sharedApplication].delegate respondsToSelector:@selector(application:handleActionWithIdentifier:forRemoteNotification:withResponseInfo:completionHandler:)]) { [[UIApplication sharedApplication].delegate application:[UIApplication sharedApplication] handleActionWithIdentifier:response.actionIdentifier forRemoteNotification:response.notification.request.content.userInfo withResponseInfo:@{} completionHandler:^{}]; } else if ([[UIApplication sharedApplication].delegate respondsToSelector:@selector(application:handleActionWithIdentifier:forRemoteNotification:completionHandler:)]){ [[UIApplication sharedApplication].delegate application:[UIApplication sharedApplication] handleActionWithIdentifier:response.actionIdentifier forRemoteNotification:response.notification.request.content.userInfo completionHandler:^{}]; } 1 } } } completionHandler(); }
當實現了新框架這兩個Delegate後,舊框架的6個Delegate將不會被執行,所以除非App只支持iOS10,盡量按上面的代碼進行兼容。
**注意-(void)application:(UIApplication )application didReceiveRemoteNotification:(NSDictionary )userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler;這個方法並未隨著舊框架被廢棄,還是正常使用。
4.App Extension
關於新通知框架的Extension,有以下兩個
NotificationService Extension
這個Extension允許我們在遠程通知收到前做一些修改。因為之前App內UNNotificaionCenter的Delegte並不能在App不存活情況下執行,所以有了這個Extension來提供這樣的功能。新建NotificationService的target後,發現系統會自動生成了UNNotificationServiceExtension的子類。其中重寫方法的模板,系統也生成好了。
@interface NotificationService () @property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver); @property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent; @end @implementation NotificationService - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { self.contentHandler = contentHandler; self.bestAttemptContent = [request.content mutableCopy]; self.contentHandler(self.bestAttemptContent); } - (void)serviceExtensionTimeWillExpire { self.contentHandler(self.bestAttemptContent); } @end
第一個方法在收到遠程通知時,可以攔截通知並修改通知內容。先用一個block保存當時的上下文,再修改完後再調用block再修改完新的通知內容回調回去。
第二個方法是在修改通知時機將要結束時調用,這時候會強制執行block了。
在推送 payload 中增加一個 mutable-content 值為 1 來啟用這個Extension,暫時還不支持本地通知。
NotificationContent Extension
這個就是實現Rich Notification的拓展。新建這個Target後,可以發現自動生成了一個ViewController並遵循了UserNotificationsUI框架的UNNotificationContentExtension協議。又是一個新框架,看來之後在通知視圖上的功能將會變得更強大。
- (void)didReceiveNotification:(UNNotification *)notification { //收到通知後的操作 } - (void)didReceiveNotificationResponse:(UNNotificationResponse *)response completionHandler:(void (^)(UNNotificationContentExtensionResponseOption option))completion{ //收到通知響應,如按鈕,可以這個方法裡攔截,並自定義操作,再決定是否將按鈕放行至主app }
這個協議裡面只有上面兩個方法。第一個方法用於在展開Rich視圖時觸發,進行根據數據去改變視圖。就如平時收到網絡請求後根據網絡請求返回的響應改變視圖元素。當打開RIch視圖後,這個方法還有兩個時機會觸發。
Request的Identifer一致,相當於更新整個通知,同時視圖也重新加載
還記得之前說的Content的threadIdentifer麼,這個相同時,這個方法也會重新調用,也就是這個是用來標識同一個流程的的通知,當然,通知還是兩條,只是視圖從通知A狀態到通知B狀態了
而第二個方法,實在通知響應時,比如按鈕響應時,先在這個Extension裡攔截,可以進行響應修改,視圖改變等操作,然後通過CompletionHandler的UNNotificationContentExtensionResponseOption來選擇放行這些響應。其中UNNotificationContentExtensionResponseOptionDismissAndForwardAction才可以將響應放行到主app裡UNNotificationCenter的delegate方法中。
同時這個Extension也有自己的Storyboard和Info.plist。
DefaultContenHidden代表是否在Rich狀態下還顯示原來的body文字
Category代表此類categoryIdentifer會觸發這個ViewController,可以是一個Array,多個同時支持這個ViewController。
SizeRatio代表高/寬比,其中寬固定為屏幕寬度,也就是用這個來調整視圖的高度。
iOS11New
都是一些小改動,增加一些細分化Api等,可以在蘋果官方文檔找到。
https://developer.apple.com/documentation/usernotifications?changes=latest_major&language=objc
總結
iOS10新通知框架和舊框架差異性
iOS10的通知框架,和之前比簡直煥然一新,整潔,優雅。還有一點需要注意的是,因為原來屬於UIKit中,那麼最好在主線程中操作。但由於老框架暫時還不會被廢棄,所以在接入時,要注意新老框架的兼容和覆蓋,避免出現不必要的Bug。
PS:此文又名《iOS10通知框架最全總結》
PPS:此文又名《iOS10通知框架,你看我就夠了》
PPPS:此文又名《iOS10通知框架,你真的懂麼?》
所有源碼和Demo
如果您覺得有幫助,不妨給個star鼓勵一下,歡迎關注&交流
有任何問題歡迎評論私信或者提issue
QQ:757765420
Email:[email protected]
Github:Nemocdz
微博:@Nemocdz
謝謝觀看
參考鏈接
活久見的重構 - iOS 10 UserNotifications 框架解析
AppleDocument-PaylodReference