UserNotifications框架詳解

UserNotificationsMind.png
無論裝置處於鎖定狀態還是使用中,都可以使用通知提供及時、重要的資訊。無論app處於foreground、background或suspended狀態,都可以使用通知傳送資訊。例如:體育類app可以使用通知告訴使用者最新比分,還可以使用通知告訴app下載資料更新介面。通知的方式有顯示橫幅(banner)、播放聲音和標記應用程式圖示。

UserNotifications.png
可以從應用程式本地生成通知,也可以從伺服器遠端生成通知。對於本地通知,app會建立通知內容,並指定觸發通知條件,如日期、倒計時或位置變化。遠端通知(remote notifications,也稱為推送通知push notifications)需要伺服器生成,由Apple Push Notification service (簡稱APNs)傳送到使用者裝置。
iOS 10 以前通知相關API在 UIApplication
或 UIApplicationDelegate
中。app在前臺時,遠端推送無法直接顯示,需要先捕獲遠端通知,然後再發起一個本地通知才能完成顯示。除此之外,app執行時和非執行時捕獲通知的路徑不同。
iOS 10 將通知集中到 UserNotifications.framework
框架,絕大部分之前通知相關API都已被標記為棄用(deprecated)。這篇文章將通過一些例子來展示iOS 10 SDK中通知相關API功能及使用方式。
1. 申請通知許可權
通知會打擾到使用者,必須先取得使用者授權。一般,可以在app啟動時請求通知許可權。
獲取 UNUserNotificationCenter
物件,呼叫 requestAuthorization(options:completionHandler:)
方法,指定通知所需互動型別。如下:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // Request notifications authorization. let center = UNUserNotificationCenter.current() center.requestAuthorization(options: [.alert, .badge, .sound, .carPlay]) { (granted, error) in print("Permission granted:\(granted)") // Enable or disable features based on authorization. } return true }
在上面程式碼中請求使用橫幅(alert)、徽標(badge)、聲音、駕駛模式通知(carPlay)四種許可權。

InteractionType.png
app第一次請求通知許可權時,系統會彈窗提醒,並記錄使用者的響應。隨後再次申請通知許可權時,系統將不再提示使用者。

UserNotificationsProm.PNG
UNUserNotificationCenter
用於管理app和app extension通知相關任務。你可以在任意執行緒同時呼叫該方法,該方法會根據任務發起時間序列執行。
當然,在使用 UserNotifications
相關API時,需要匯入 UserNotifications
框架:
import UserNotifications
2. 本地通知
一般使用本地通知引起使用者注意。例如,後臺app可以在任務完成時顯示通知。始終使用本地通知傳達與使用者相關的重要資訊。
系統會按照app指定的觸發條件(如時間、位置)來傳遞通知。如果傳送通知時,app處於background或suspend,系統會代替app與使用者互動;如果app處於foreground,系統會將通知遞交至app進行處理。
2.1 建立通知內容
要為本地通知指定payload,需要建立 UNMutableNotificationContent
物件。使用 UNMutableNotificationContent
物件為banner指定title、subtitle、body,通知聲音,以及app徽標數值。
// Configure the notificaiton's payload. let content = UNMutableNotificationContent() content.title = "Calendar Title" content.subtitle = "This is subtitle" content.body = "This is body" content.sound = UNNotificationSound.default() content.badge = 1
通知中顯示的文字應當進行本地化。儘管本地化時可以使用 NSLocalizedString
巨集,但更好的選擇是使用 NSString
物件的 localizedUserNotificationString(forKey:argumentse)
方法。該方法可以在改變系統語言後,更新已顯示通知語言。
content.title = NSString.localizedUserNotificationString(forKey: "Calendar Title", arguments: nil)
2.2 指定本地通知觸發條件
本地通知觸發條件有以下三種:
- UNCalendarNotificationTrigger
- UNTimeIntervalNotificationTrigger
- UNLocationNotificationTrigger
每個觸發條件需要不同的引數。例如,基於日曆的觸發器需要指定傳送通知的日期和時間。
2.2.1 UNCalendarNotificationTrigger
使用 UNCalendarNotificationTrigger
物件可以在指定日期和時間觸發本地通知,使用 NSDateComponents
物件指定相關日期資訊。系統會在指定日期、時間傳遞通知。
下面建立了一個每天早7點30分的提醒:
var date = DateComponents() date.hour = 7 date.minute = 30 let trigger = UNCalendarNotificationTrigger(dateMatching: date, repeats: true)
上面的 repeats
引數指定每天7點30分進行提醒。
也可以根據使用者選擇的Date Picker設定trigger:
@IBAction func datePickerDidSelectNewDate(_ sender: UIDatePicker) { let date = sender.date let calendar = Calendar(identifier: .chinese) let components = calendar.dateComponents(in: .current, from: date) let newComponents = DateComponents(calendar: calendar, timeZone: .current, month: components.month, day: components.day, hour: components.hour, minute: components.minute) let trigger = UNCalendarNotificationTrigger(dateMatching: newComponents, repeats: false) }
關於 UNCalendarNotificationTrigger
的使用,可以參考demo中 CalendarViewController.swift
部分內容。
如果你在用Objective-C ,可以下載demo獲取Objective-C版本程式碼。文章底部原始碼地址包含了Swift和Objective-C兩種demo。
2.2.2 UNTimeIntervalNotificationTrigger
指定時間流逝(elapse)後觸發通知,計時器使用這種型別的觸發器。
下面建立一個2分鐘後提醒一次的觸發器:
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 2*60, repeats: false)
關於 UNTimeIntervalNotificationTrigger
的使用,可以參考demo中 TimeIntervalViewController.swift
部分內容。
2.2.3 UNLocationNotificationTrigger
當用戶裝置進入或離開指定地理區域時觸發通知。例如,iPhone中 提醒事項 app的 在指定位置提醒我 功能。系統對所新增基於位置觸發器數量有限制。
使用位置觸發器前,你的app必須取得使用Core Location定位的許可權。事實上,系統負責監控是否進入、離開指定地理區域。
雖然 ofollow,noindex">文件 要求只取得 使用期間when-in-use 獲取地理位置的許可權即可,但經測試和 搜尋 ,必須獲得 始終 always 獲取地理位置的許可權。如果是我的錯誤,特別感謝 反饋 指出。
配置region時,使用 notifyOnEntry
和 notifyOnExit
屬性指定進入或離開時觸發提醒,也可以兩者都提醒。下面程式碼指定離開時進行提醒:
// Creating a location-based trigger. let center = CLLocationCoordinate2DMake(39.9042, 116.4074) let region = CLCircularRegion(center: center, radius: 500, identifier: "Headquarters") region.notifyOnExit = true region.notifyOnEntry = false let trigger = UNLocationNotificationTrigger(region: region, repeats: false)
Demo中使用的是當前裝置位置,具體內容可以檢視 LocationViewController.swift
部分。
基於地理區域的通知並不會在裝置剛剛離開區域邊界時觸發。系統使用啟發式(heuristic)方法確保裝置離開指定區域,而非定位有誤導致。
2.3 建立並註冊UNNotificationRequest
使用上面的 UNMutableNotificationCentent
和觸發器建立 UNNotificationRequest
物件,並通過 add(_:withCompletionHandler:)
方法將通知傳遞至系統。
// Create the request. let request = UNNotificationRequest(identifier: "calendar", content: content, trigger: trigger) // Schedule the request with the system. UNUserNotificationCenter.current().add(request) { (error) in if let error = error { print("Failed to add request to notification center. error:\(error)") } }
Demo中觸發器建立通知如下:

UserNotificationsCalendar.gif

UserNotificationsTimeInterval.gif
通知到達時app若處於前臺,預設不會彈出alert。因此,需要設定通知後立即進入後臺。
2.4 取消未展示、已展示通知
通知請求提交後將保持活躍狀態,直到滿足其觸發條件,或顯式取消。通常,在條件變化時取消、更新通知。例如,使用者提前完成了提醒,你將取消與該提醒相關的所有通知請求;使用者更改了提醒,你將更新其觸發條件。
使用 UNUserNotificationCenter
呼叫 removePendingNotificationRequests(withIdentifiers:)
方法取消通知請求。如需更新通知,只需使用相同標誌符建立request即可。
- 取消還未展示的通知:使用
UNUserNotificationCenter
呼叫removePendingNotificationRequests(withIdentifiers:)
方法 - 更新未展示通知:使用對應request標誌符建立新通知。
- 取消已展示的通知:使用
UNUserNotificationCenter
呼叫removeDeliveredNotifications(withIdentifiers:)
方法。 - 更新已展示通知:使用對應request標誌符建立新通知。
3. 遠端通知
通過支援遠端通知,可以向用戶提供最新資訊,即時app並未執行。為了能夠接收和處理遠端通知,需遵守以下步驟:
- 開啟遠端通知
- 在Apple Push Notification service (APNs)註冊並接收app的device token.
- 傳送device token至提供通知的伺服器。
- 對傳入的通知進行處理。
3.1 開啟遠端通知
只有付費的開發者賬號才可以使用遠端通知。點選Project navigation中工程名稱,選擇Capacities選項卡,開啟 Push Notifications 功能:

UserNotificationsCapacities.png
未付費開發者賬號不顯示 Push Notifications選項。
開啟該選項後,Xcode會自動新增entitlement,你可以登入 開發者中心 檢視app IDs,會顯示應用Push Notifications 待配置。

UserNotificationsConfigurable.png
點選底部 edit 按鈕,滑動到Push Notifications部分:

UserNotificationsEditPushNotifications.png
在 Development SSL Certificate 部分,點選 Create Certificate 按鈕,根據提示建立CSR。根據提示用上一步建立的CSR生成你的證書,最後下載生成的證書並匯入到Keychain:

UserNotificationsCert.png
返回到開發者賬號Identifiers > App IDs部分,應用push notifications Development已經可用:

UserNotificationsPushNotificationsEnabled.png
現在,使用Keychain證書就可以傳送通知了。
3.2 在APNs註冊並接收device token
每次app啟動,都需要在APNs註冊。不同平臺註冊方法稍有不同,但其步驟是相似的:
- app請求在APNs註冊。
- 註冊成功後,APNs傳送針對該app的device token至該裝置。
- 系統呼叫app中delegate方法,並將device token傳遞給該方法。
- app將device token傳遞至伺服器。
應用程式的device token是全域性唯一的,標誌特定app、裝置的組合。收到device token後,需要由你將device token及其他相關資料(例如,使用者身份資訊)傳送至伺服器。稍後傳送遠端通知時,必須附帶device token和通知payload。
不要在app內快取device token,每次需要時呼叫系統方法獲取。出現以下情況時,APNs會向你的app發出新的device token:
- 使用者從備份中恢復系統。
- 使用者在新裝置安裝了你的app。
- 使用者重新安裝了作業系統。
當裝置令牌沒有發生變化時,獲取裝置令牌的方法會快速返回。
當device token變化後,使用者必須開啟你的app,以便獲取更新後的device token,APNs才可以向你的裝置傳送遠端通知。
watchOS裝置中app不需要註冊獲取遠端通知,其依靠與其配對的iPhone轉發遠端通知。當iPhone處於鎖定或螢幕處於關閉狀態,Apple Watch在使用者手腕中且未鎖定時,iPhone會自動轉發通知至Apple Watch。
在iOS和tvOS中,通過呼叫 UIApplication
物件的 registerForRemoteNotifications
方法來向APNs註冊。通常,在啟動時呼叫此方法作為正常啟動序列的一部分。app第一次呼叫此方法時會聯絡APNs,請求該app在此裝置的device token。隨後系統會非同步呼叫下面兩個方法之一:
application(_:didRegisterForRemoteNotificationsWithDeviceToken:) application(_:didFailToRegisterForRemoteNotificationsWithError:)
APNs的device token長度可變,不要硬編碼其大小。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // Request notifications authorization. let center = UNUserNotificationCenter.current() center.requestAuthorization(options: [.alert, .badge, .sound, .carPlay]) { (granted, error) in print("Permission granted:\(granted)") guard granted else { return } // Register for push notification. UIApplication.shared.registerForRemoteNotifications() } return true } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { // Receive device token let token = deviceToken.map { data -> String in return String(format: "%02.2hhx", data) }.joined() print("Device Token:\(token)") // Forward token to server. // Enable remote notification features. } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { print("Fail to register for remote notifications. error:\(error)") // Disable remote notification features. }
在裝置中執行,輸出如下:
Permission granted:true Device Token:7720b86184fa24100375f2e773b9bd201130eb41aaf2a28fae1157593d1592f0
3.3 傳送遠端通知至APNs
遠端通知從你公司伺服器開始,由伺服器決定何時向用戶傳送通知。遠端通知包含通知資料和使用者裝置唯一標誌符的請求。將遠端通知轉發給APNs,APNs負責將通知傳遞至使用者裝置。收到通知後,使用者裝置的作業系統會處理使用者互動,並將通知傳遞給app。

UserNotificationsAPNs.png
配置伺服器部分不在這篇文章範圍內,你可以檢視 Setting Up a Remote Notification Server 這篇文件。
使用Pusher傳送遠端通知
傳送推送通知需要與APNs建立SSL連結,並使用剛建立的證書,Pusher可以完成這一任務。這篇文章中將使用Pusher向裝置傳送遠端通知,你可以根據 這裡 的介紹安裝使用。
啟動Pusher,Pusher會自動檢查Keychain中證書,並在下拉選單中列舉。詳細步驟如下:
-
從下拉選單中選擇證書。
-
將控制檯輸出的device token貼上至 Device push token 文字框。
-
修改遠端推送請求如下:
{ "aps":{ "alert":{ "title":"This is Title", "body":"This is Body" }, "badge":1, "sound":"default" } }
-
開啟執行 UserNotification Swift 的裝置,將app放到後臺,或鎖定裝置螢幕,否則會不顯示通知。
-
點選Pusher的 Push 按鈕。

UserNotificationsPusher.png
現在,你收到了第一條遠端通知。

UserNotificationsInitial.png
4. Payload
每個通知都需要提供payload和device token,以及呈現通知的細節。Payload是在伺服器上建立的JSON字典物件。JSON字典必須包含 aps
鍵,其值是通知的資料。 aps
鍵的內容決定系統使用那種樣式呈現通知:
- 彈出橫幅。
- 為應用圖示增加徽標。
- 播放聲音。
- 以靜默方式傳送通知。
在 aps
字典之外,JSON詞典還可以使用自定義key、value。自定義的value必須使用JSON結構,並僅使用基本型別(primitive type),如dictionary、array、string、number和Boolean。請勿在payload中包含敏感資料,除非該資料已被加密,或只對當前應用環境有效。例如,payload可以包含一個會話標誌符,即時訊息app使用該標誌符定位相應使用者對話。通知中的資料永遠不應具有破壞性,也就是說,app不應使用通知來刪除使用者裝置上的資料。
4.1 APS詞典Keys
Apple使用 aps
詞典內鍵傳遞通知至使用者裝置, aps
詞典中鍵內容決定通知與使用者的互動方式。下表列出了 aps
詞典可用的key,除此之外的 key
都會被系統忽略。
Key | Value type | Comment |
---|---|---|
alert |
Dictionary or String | 使用該key彈出橫幅。 該鍵的首選值是字典型別,該字典可選key在下一部分。如果指定字串作為此key的值,則該字串將顯示為橫幅的訊息文字。 alert中文字不支援 \U 表示法,請使用UTF-8。 |
badge |
Number | 使用該key修改應用圖示徽標。 如果沒有包含該key,則badge不變。要移除badge,設定其值為 0 。 |
sound |
String | 使用該key播放聲音,其值為app main bundle或Library/Sounds目錄聲音檔名稱。如果無法找到該檔案,或指定為 default ,系統播放預設聲音。 更多關於通知聲音檔案資訊,可以檢視 Preparing Custom Alert Sounds |
content-available |
Number | 包含該key,並將值設定為 1 ,用於設定後臺更新通知。當該通知到達時,系統會喚醒你的應用,並將通知傳遞給app。 更多關於background update notifications的資訊,檢視 Configuring a Background Update Notification |
category |
String | 該鍵的字串表示通知互動型別,該鍵值的字串對應app註冊的category。 |
thread-id |
String | 該key用於分組通知內容。可以在Notification Content app extension中,使用該鍵的值將通知分組。對於本地通知,使用 UNNotificationContent 物件的 threadIdentifier 屬性。 |
mutable-content |
Number | Notification service擴充套件。當值為 1 時,系統將通知傳遞給notification service擴充套件,使用notification service擴充套件修改通知內容。 |
使用者裝置中關於通知的設定最終決定是否使用alert、badge和sound進行提醒。
4.2 Alert Keys
下表列出了 alert
詞典接收的key,及相應value型別。
Key | Value type | Comment |
---|---|---|
title |
String | 描述通知的簡簡訊息。 |
body |
String | Alert通知內容。 |
title-loc-key |
String 或 null |
使用該key本地化Title字串,其值在 Localizable.strings 檔案中。key字串可以使用 %@ 和 %n$@ 格式符,以獲取 title-loc-args 陣列中的變數。 |
title-loc-args |
元素為字串的陣列 或 null |
替換 title-loc-key 中變數。 |
5. 可操作(Actionable)通知傳送和處理
Actionable notifications允許使用者直接響應通知,而無需啟動app。其他型別通知只顯示通知資訊,使用者唯一的操作是啟動app。對於可操作的通知,除通知介面外,系統還會顯示一個或多個按鈕。點選按鈕會將所選操作傳送到app,然後app在後臺處理操作。

UserNotificationsActionable.png
可互動式通知是通過將一簇 UNNotificationAction
放到一個 UNNotificationCategory
中,在app啟動時註冊category,傳送通知時將要使用category標誌符新增到payload中實現的。
5.1 註冊可互動式通知
UNNotificationCategory
定義app支援的互動式操作型別, UNNotificationAction
定義每種category可操作按鈕。
使用 init(identifier:actions:intentIdentifiers:options)
方法建立category,其中 identifier
屬性是category最重要部分,當生成通知時,payload必須包含該標誌符。系統使用該 identifier
屬性定位category和對應action。
使用 init(identifier:title:options:)
建立action,當用戶點選按鈕時,系統將 identifier
轉發給你的app, options
引數指定按鈕行為。例如:執行刪除內容操作時,使用 destructive
樣式;需要啟動app時,使用 foreground
樣式;只允許未鎖定裝置上執行,使用 authenticationRequired
樣式。
下面為 CalendarViewController
新增稍後提醒操作:
private func registerNotificationCategory() { // calendarCategory let completeAction = UNNotificationAction(identifier: "markAsCompleted", title: "Mark as Completed", options: []) let remindMeIn1MinuteAction = UNNotificationAction(identifier: "remindMeIn1Minute", title: "Remind me in 1 Minute", options: []) let remindMeIn5MinutesAction = UNNotificationAction(identifier: "remindMeIn5Minutes", title: "Remind me in 5 Minutes", options: []) let calendarCategory = UNNotificationCategory(identifier: "calendarCategory", actions: [completeAction, remindMeIn5MinutesAction, remindMeIn1MinuteAction], intentIdentifiers: [], options: [.customDismissAction]) UNUserNotificationCenter.current().setNotificationCategories([calendarCategory]) }
記得在 application(_:didFinishLaunchingWithOptions:)
方法呼叫上述方法。
所有action物件都必須具有唯一標誌符。處理action時,標誌符是區分一個操作與另一個操作的唯一方法。
5.2 payload中新增category
只有payload中包含有效category identifier,通知才會顯示action。系統使用payload中category identifier在已註冊category和action中查詢,使用查詢到的資訊為通知新增action按鈕。
要為本地通知新增category,將相應字串分配給 UNMutableNotificationContent
物件的 categoryIdentifier
屬性。
為上面建立的 CalendarViewController
新增category:
content.categoryIdentifier = "calendarCategory"
詳細內容,可以檢視demo中 CalendarViewController.swift
部分內容。
執行如下:

UserNotificationsCategoryIdentifier.gif
要為遠端通知新增category,只需為JSON payload中 aps
字典新增 category
key即可。
6. 響應通知
目前為止,點選通知alert只會開啟app,Action中按鈕也無法點選;app處於前臺時也無法收到通知。
UNUserNotificationCenterDelegate
協議中的方法用來處理與通知的互動,以及app處於前臺時如何響應通知。
userNotification(_:didReceive:withCompletionHandler:) userNotificationCenter(_:willPresent:withCompletionHandler:)
6.1 處理actionable通知
當用戶點選通知中action時,系統會在後臺啟動你的app,並呼叫 userNotification(_:didReceive:withCompletionHandler:)
方法,在該方法內將 response
的 actionIdentifier
與註冊action標誌符進行匹配。如果使用者使用系統預設互動開啟app,或清除通知,系統會自動匹配 UNNotificationDefaultActionIdentifier
和 UNNotificationDismissActionIdentifier
。
下面實現了 CalendarViewController
中actionable通知:
// Use this method to process the user's response to a notification. func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { if response.actionIdentifier == UNNotificationDefaultActionIdentifier { print("Default Action") } else if (response.actionIdentifier == UNNotificationDismissActionIdentifier){ print("Dismiss action") }else if (response.notification.request.content.categoryIdentifier == "calendarCategory") { handleCalendarCategory(response: response) } UIApplication.shared.applicationIconBadgeNumber = 0 completionHandler() } private func handleCalendarCategory(response: UNNotificationResponse) { if response.actionIdentifier == "markAsCompleted" { } else if response.actionIdentifier == "remindMeIn1Minute" { // 1 Minute let newDate = Date(timeInterval: 60, since: Date()) scheduleNotification(at: newDate) } else if response.actionIdentifier == "remindMeIn1Minute" { // 5 Minutes let newDate = Date(timeInterval: 60*5, since: Date()) scheduleNotification(at: newDate) } }
執行demo, CalendarViewController
通知中的 Mark as Completed 、 Remind me in 1 Minute 和 Remind me in 5 Minutes 按鈕將可以使用。
如果是actionable通知,使用者 清楚 通知,或通過通知開啟app時,會呼叫系統預定義的 UNNotificationDismissAction
或 UNNotificationDefaultActionIdentifier
。
關於 categoryIdentifier
和 actionIdentifier
的使用,可以參考demo中 NotificationHandler.swift
部分內容。
6.2 應用內展示通知
現在,app處於後臺或殺死時可以顯示通知,並響應actionable通知。但app處於前臺時,收到的通知是無法顯示的。如果希望在應用內也顯示通知的話,需要額外工作。
當app處於前臺時,通知到達時系統會呼叫 UNNotificationCenterDelegate
協議的 userNotificationCenter(_:willPresent:withCompletionHandler:)
方法,使用該方法處理通知,並使用 completionHandler()
告知系統所需的提醒方式。
設定Calendar通知只有app處於後臺時才顯示,其它通知直接顯示。
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { let identifier = notification.request.identifier let options: UNNotificationPresentationOptions if identifier == "calendar" { options = [] } else { options = [.alert, .sound] } completionHandler(options) }
如果你的app註冊使用了 PushKit
, PushKit
型別的通知會直接傳送至app,不會直接顯示給使用者。如果app處於foreground或background,系統會為你的應用提供處理通知的時間;如果你的應用未執行,系統會在後臺啟動你的應用,以便app可以處理通知。
6.3 設定代理物件
實現 UNUserNotificationCenterDelegate
協議方法後,將其分配給 UNUserNotificationCenter
物件的 delegate
屬性。
let notificationHandler = NotificationHandler() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { ... UNUserNotificationCenter.current().delegate = notificationHandler return true }
在app啟動完畢前,必須將代理物件分配給 UNUserNotificationCenter
物件。例如,在iOS應用中,必須在 applicaiton(_:willFinishLaunchingWithOptions:)
或 application(_:didFinishLaunchingWithOptions:)
方法之一設定代理。在此之後設定代理,可能導致應用錯過通知。
7. Notification Service Extension
有時需要在使用者iOS裝置上修改遠端通知內容:
- 在伺服器推送payload中加入加密文字,在客戶端接收到通知後進行解密,以便完成端到端(end-to-end)的加密。
- 使用HTTP/2傳送的payload,大小不得超過4KB。可以通過在payload中新增url,顯示通知前下載附件解決這一問題。
- 顯示通知前,使用使用者裝置上資料,更新通知內容。
想要修改遠端通知內容,需要使用notification service應用擴充套件。在使用者收到通知之前,Notification service app extension有約30秒時間來處理遠端通知。
Notification service app extensions只修改遠端通知的內容。如果使用者禁用了app通知,或payload只使用sound、badge提醒使用者,或通知為silent通知,該擴充套件不會響應。
如果你還不瞭解擴充套件,可以檢視 Today Extension(widget)的使用 這篇文章。
7.1 為工程新增service extension
和其它擴充套件一樣,notification service app extension在你的iOS應用中處於獨立bundle。新增該擴充套件步驟如下:
- 選擇Xcode中的 File > New > Target...
- 選取 iOS > Application Extension 中的Notification Service Extension。點選 Next 。
- 指定副檔名稱和其它資訊。點選 Finish 。
Xcode中Notification Service Extension模板提供了 NotificationService.swift
檔案和 info.plist
檔案。在 info.plist
檔案中,已自動為 NSExtensionPointIdentifier
鍵設 com.apple.usernotifications.service 值。
7.2 實現擴充套件的handler methods
Notification service應用擴充套件提供了以下方法用於修改遠端通知內容:
-
didReceive(_:withContentHandler:)
:該方法必須實現。通過重寫該方法修改遠端通知的UNNotificationContent
。修改通知內容後呼叫content handler塊。如果決定放棄修改通知內容,在呼叫contentHandler
塊時傳入request
原始內容。 -
serviceExtensionTimeWillExpire()
:該方法可選實現,但強烈推薦實現。didReceive(_:withContentHandler:)
方法佔用太長時間執行任務時,系統會在單獨執行緒呼叫該方法,以便提供最後一次修改通知的機會。此時,應當儘快呼叫contentHandler
。例如,如果擴充套件正在下載圖片,你可以更新通知alert文字,提示使用者有正在下載的圖片。如果你沒有及時呼叫didReceive(_:withContentHandler:)
方法中的完成處理程式,系統會顯示通知的原始內容。
進入 NotificationService.swift
檔案,實現 didReceive(_:withContentHandler:)
方法:
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) bestAttemptContent?.body = "\(bestAttemptContent?.body ?? "Default Body") pro648" contentHandler(bestAttemptContent) }
7.3 配置payload
當遠端通知滿足以下條件時,系統才會呼叫notification service app extension:
- Payload必須包含
mutable-content
key,且其值為1
。 - Payload的alert字典需包含title、subtitle或body這些資訊,即通知必須顯示banner。
遠端通知payload如下:
{ "aps" : { "alert" : { "title" : "This is title", "body" : "This is body", }, "badge" : 2, "sound" : "default", "mutable-content" : 1, }, }
傳送通知,如下所示:

UserNotificationsMutable-content.gif
使用notification service app extension,可以修改通知中任何內容。可以下載圖片、視訊,並將其作為attachment新增至通知content。你也可以修改alert內容,但不能移除alert內容。如果通知content不包含alert,系統會忽略你的修改,直接原樣呈現。
在payload中傳送圖片網址,以附件方式顯示圖片:
{ "aps" : { "alert" : { "title" : "This is title", "body" : "This is body", }, "badge" : 2, "sound" : "default", "mutable-content" : 1, }, "media-url" : "https://raw.githubusercontent.com/wiki/pro648/tips/images/UNA.jpg", }
更新 didReceive(_:withContentHandler:)
方法如下:
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) // Dig in the payload to get the attachment-url. guard let bestAttemptContent = bestAttemptContent, let attachmentURLAsString = request.content.userInfo["media-url"] as? String, let attachmentURL = URL(string: attachmentURLAsString) else { return } // Download the image and pass it to attachments if not nil. downloadImageFrom(url: attachmentURL) {(attachment) in if attachment != nil { bestAttemptContent.attachments = [attachment!] contentHandler(bestAttemptContent) } } } extension NotificationService { private func downloadImageFrom(url: URL, with completionHandler: @escaping (UNNotificationAttachment?) -> Void) { let task = URLSession.shared.downloadTask(with: url) { (location, response, error) in // 1. Test URL and escape if URL not OK guard let location = location else { completionHandler(nil) return } // 2. Get current's user temporary directory path var urlPath = URL(fileURLWithPath: NSTemporaryDirectory()) // 3. Add proper ending to url path, in the case .jpg (The system validates the content of attached files before scheduling the corresponding notification request. If an attached file is corrupted, invalid, or of an unsupported file type, the notificaiton request is not scheduled for delivery.) let uniqueURLString = ProcessInfo.processInfo.globallyUniqueString + ".png" urlPath = urlPath.appendingPathComponent(uniqueURLString) // 4. Move downloadUrl to newly created urlPath try? FileManager.default.moveItem(at: location, to: urlPath) // 5. Try adding the attachement and pass it to the completion handler do { let attachment = try UNNotificationAttachment(identifier: "picture", url: urlPath, options: nil) completionHandler(attachment) } catch { completionHandler(nil) } } task.resume() } }
執行demo,傳送遠端通知。如下:

UserNotificationsMediaURL.gif
擴充套件修改通知內容、呼叫 contentHandler
方法的時間不得超過30秒。如果沒有及時呼叫 contentHandler
方法,系統會呼叫 serviceExtensionTimeWillExpire()
方法,這是你最後一次修改content的機會。
override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { bestAttemptContent.title = "Incoming Image" contentHandler(bestAttemptContent) } }
8. UserNotificationsUI
當iOS裝置收到通知時,系統分兩個階段顯示alert。
- 最初,其會顯示一個標題、副標題,以及兩到四行正文。
- 使用者用力按壓alert,或下劃alert時,iOS將顯示完整通知,包含響應按鈕。
系統提供略縮alert介面,你可以使用 UserNotificationUI
自定義完整通知介面。

UserNotificationsUI.png
新增notification content extension方法步驟和notification service extension。選擇Xcode中 File > New > Target...,選擇 iOS > Application Extension > Notification Content Extonension模板。
8.1 Info.plist檔案
一個工程可以新增多個notification content service,但每一個擴充套件至少支援一個單獨category。在 Info.plist
檔案中使用 UNNotificationExtensionCategory
key宣告所支援category。其value預設為String型別。如果要支援多個category,可以將其修改為Array型別。

UserNotificationsContentInfo.png
這裡將 UNNotificationExtensionCategory
key的value設定為 customUICategory
。
Notification content app extension中 Info.plist
鍵如下:
Key | Value |
---|---|
UNNotificationExtensionCategory (Required) |
字串,或包含字串的陣列。字串為使用 UNNotificationCategory 類宣告category的標誌符。 |
UNNotificationExtensionInitialContentSizeRatio (Required) |
浮點值型別,表示檢視控制器檢視初始大小,為高度與寬度的比率。在載入擴充套件時,系統使用此值設定檢視控制器初始大小。例如,值為 0.5 時,檢視控制器高度為其寬度的一半。載入擴充套件後也可以更改檢視控制器大小。 |
UNNotificationExtensionDefaultContentHidden | 布林型別,預設為 NO 。設定為 YES 時,系統只顯示自定義通知介面;設定為 NO 時,系統會顯示預設通知介面。自定義action按鈕和取消按鈕會永遠顯示。 |
UNNotificationExtensionOverridesDefaultTitle | 布林型別,預設為 NO 。設定為 YES 時,系統將控制器的title設定為通知title;設定為 NO 時,系統將app名稱設定為通知title。 |
下圖左側為未隱藏default notification interface,右側為隱藏後介面:

UNNotificationsDefaultNotificationInterface.png
8.2 新增檢視
Notification content extension 模板包含一個storyboard,通過為storyboard新增檢視來構建自定義通知介面。例如,使用 UILabel
顯示通知title、subtitle和body。還可以新增圖片、視訊等非互動式內容,無需為檢視提供任何初始內容。
在iOS 12及以後,還可以通過為 Info.plist
新增 UNNotificationExtensionUserInteractionEnabled
key,啟用互動式控制元件,如 UIButton
、 UISwitch
。該key為Boolean型別,值為 YES
時支援互動式控制元件。
不要新增多個檢視控制器,notification content app extension 只支援使用一個檢視控制器。
設定storyboard高度為 160
,並新增 UILabel
,如下:

UserNotificationsContentStoryboard.png
8.3 UNNotificationExtension協議
UNNotificationContentExtension
協議為notification content app extension提供了入口,用於提供自定義通知頁面。Notification Content Extension模板提供的 NotificationViewController
類遵守該協議。該協議方法如下:
-
didReceive(_:)
:該方法必須實現。在該方法內使用notification content配置檢視控制器。在檢視控制器可見時,該方法可能會被呼叫多次。具體的說,新到達通知與已經顯示通知threadIdentifier
相同時,會再次呼叫該方法。該方法在擴充套件程式的主執行緒中呼叫。 -
didReceive(_:completionHandler:)
:該方法可選實現。使用者點選自定義按鈕時會呼叫該方法。該方法的UNNotificationResponse
引數可以用來區分使用者點選的按鈕。處理完畢任務後,必須呼叫completion
塊。如果你實現了該方法,則必須處理所有category的所有action。如果沒有實現該方法,使用者點選按鈕後系統會將通知轉發給你的app。
只可以修改 NotificationViewController
檢視的高度,不可修改其寬度。
實現 didReceive(_:)
方法,根據通知內容設定通知標題:
func didReceive(_ notification: UNNotification) { //title = "pro648" self.label?.text = String("Content Extension:\(notification.request.content.body)") }
如下所示:

UserNotificationsContentBody.gif
可以看到,自定義檢視高度與寬度相等。修改 UNNotificationExtensionInitialContentSizeRatio
值為 0.5
,設定 UNNotificationExtensionDefaultContentHidden
值為 YES
,設定 UNNotificationExtensionOverridesDefaultTitle
值為 YES
,並取消上述程式碼中設定檢視控制器title程式碼,執行併發送遠端通知:

UNNotificationsContentAttributes.gif
更新 didReceive(_:)
方法,在顯示通知自定義介面時,搖動 speakerLabel
;實現 didReceive(_:completionHandler:)
方法,點選 Stop 按鈕時,停止搖動 speakerLabel
, Comment 按鈕為 UNTextInputNotificationAction
型別:
func didReceive(_ notification: UNNotification) { title = "pro648" self.label?.text = String("Content Extension:\(notification.request.content.body)") speakerLabel.shake() } func didReceive(_ response: UNNotificationResponse, completionHandler completion: @escaping (UNNotificationContentExtensionResponseOption) -> Void) { if response.actionIdentifier == "stop" { speakerLabel.text = ":mute:" speakerLabel.cancelShake() completion(.doNotDismiss) } else if response.actionIdentifier == "comment" { completion(.dismissAndForwardAction) } else { completion(.dismiss) } } } extension UIView { func shake() { let animation = CAKeyframeAnimation(keyPath: "transform.translation.x") animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) animation.duration = 1 animation.repeatCount = .infinity animation.values = [-20.0, 20.0, -20.0, 20.0, -10.0, 10.0, -5.0, 5.0, 0.0] layer.add(animation, forKey: "shake") } func cancelShake() { layer.removeAnimation(forKey: "shake") } }
執行如下:

UserNotificationsShake.gif
註冊 customUICategory
category和註冊 calendarCategory
類似,可以檢視原始碼 AppDelegate.swift
部分。
9. 關於使用者體驗
- 使用完整的句子,正確的標點符號,提供有價值資訊。不要截斷通知中的資訊,雖然在資訊過長時,系統會進行這樣的操作。避免告訴使用者導航到特別頁面,點選特定按鈕,或執行其它難以記住的任務。
- 即時使用者沒有響應通知,也不要為同一任務傳送多個通知。使用者會在方便時處理通知,如果你的app為同一件事情傳送多個通知,會佔用通知中心整個介面,使用者可能會關閉你的app通知功能。
- 通知中不要包含app名稱和icon。系統會自動在每個通知頂部顯示這些資訊。
- 考慮提供詳細檢視。通過詳細檢視,可以在當前環境對通知進行操作,無需開啟app。此檢視應易識別、包含有價值資訊,就像app正常擴充套件。其可以包含圖片、視訊和其它內容,並可以在顯示時動態更新。
- 避擴音供破壞性功能。提供破壞性操作前,需要確保使用者對該操作有清晰認識,不存在誤解。destructive型別操作會用紅色顯示。
Demo名稱:UserNotifications
原始碼地址: https://github.com/pro648/BasicDemos-iOS/tree/master/UserNotifications
參考資料:
- Push Notifications Tutorial: Getting Started
- iOS 10 rich push notifications with media attachments
- Local and Remote Notification Programming Guide
- 活久見的重構 - iOS 10 UserNotifications 框架解析
- iOS 10 Day by Day :: Day 6 :: Notification Content Extensions
- UNLocationNotificationTrigger- Not working in simulator
- How can I convert my device token (NSData) into an NSString?
- Introduction to Notifications