iOS 10 最重要的變化可能就是通知 API 的重構了。本文用一個簡單鬧鐘的例子介紹了 User Notification 的 API 變化和新功能。javascript
《iOS 10 day by day》是 shinobicontrols 公司編寫的系列博客,介紹開發者須要瞭解的 iOS 10 新特性,每週更新。本系列翻譯(文集地址)已取得官方受權。目錄點此。倉薯翻譯,歡迎指正:)java
Shinobicontrols 爲 iOS 和 Android 開發者提供高性能、響應式的 UI 控件 SDK,尤爲是圖表方面的控件。 官網 : shinobicontrols.com twitter : @shinobicontrolsios
好久之前,開發者就能夠在 iOS 裏預定本地通知了,可是以前的 API 缺少細粒度的控制能力。幸運的是,蘋果在 iOS 10 中改善了這一點,發佈了新的 UserNotifications
框架。這個框架在處理本地通知及遠程推送方面的 API 豐富了許多,同時寫法更加簡便。git
本地通知(local notification)是用 app 來預定的通知,例如:提醒你帶午餐的鬧鐘。而遠程推送(remote notification)通常是服務器發起的,傳到蘋果的 APNS 服務器上,APNS 再推送到用戶手機上。例如:推送給全部用戶,告訴他們 app 發佈新版本了。github
工程是用 Xcode 8 Beta 6 建的api
咱們用一個簡單的鬧鐘 app 來介紹新的 UserNotification
框架,一個用戶能夠預定提醒的 to do list。到時間後,鬧鐘每 60 秒提醒一次,直到用戶手動取消爲止。跟以前同樣,代碼放在 github 上。數組
每一個小喇叭的圖標表示一個預定好的提醒,而被紅色斜槓劃掉的小喇叭表示這個事項不須要提醒。服務器
咱們還會添加讓用戶對通知作出響應的功能:閉包
UI 界面上就是一個簡單的 tableView,顯示用戶的 to do list。沒什麼可說的。app
提醒事項的數據類型是這樣定義的:
class NagMeTableViewController: UITableViewController {
typealias Task = String
let tasks: [Task] = [
"Wash Up",
"Walk Dog",
"Exercise"
]
// 待續複製代碼
咱們的 tableView 就是一個提醒事項的列表,點擊 cell 上的小喇叭按鈕會調用一個閉包。
// 續上
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCell", for: indexPath) as! TaskCell
let task = tasks[indexPath.row]
cell.nameLabel.text = task
// 顯示 cell 上提醒/不提醒的圖標
retrieveNotification(for: task) {
request in
request != nil ? cell.showReminderOnIcon() : cell.showReminderOffIcon()
}
// 點擊按鈕時調用閉包
cell.onButtonSelection = {
[unowned self] in
self.toggleReminder(for: task)
}
return cell
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tasks.count
}
}複製代碼
爲了判斷用戶是否是當前『正在被提醒』,咱們要調一個 retrieveNotification(for: task)
方法,待會再詳細說。若是存在 notification 對象,說明用戶要求提醒這個事項。
當點擊 cell 上喇叭按鈕的時候,會調用一個 toggleReminder(for: task)
方法,咱們也放在後文介紹。這個方法裏就是預定提醒的神奇魔法。
在預定提醒以前,須要先向用戶請求通知的受權。在 app 啓動時調用以下代碼:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().requestAuthorization(options: [.sound, .alert]) {
granted, error in
if granted {
print("Approval granted to send notifications")
}
}
}複製代碼
調用的結果是會顯示一個彈窗,詢問用戶是否容許咱們的 app 發送通知。閉包的 granted
參數表示咱們是否取到了權限。這個彈窗只會顯示一次,不過以後用戶也能夠在設置裏進行更改。
你會發現,User Notification
框架大量的 API 使用了 completion block。這是由於向 UNUserNotificationCenter
發出的請求大部分都是在後臺線程上異步執行的。調用 current()
方法會讓框架返回一個供咱們 app 使用的 notification center 單例對象,而咱們全部的預定通知、取消通知都要經過這個單例對象來實現。
建立、添加通知的過程實在有些冗長,咱們把代碼分解成幾部分,一步一步來看:
/// 爲 task 建立一個 notification,每分鐘重複一次
func createReminderNotification(for task: Task) {
// 配置 notification 的 content
let content = UNMutableNotificationContent()
content.title = "Task Reminder"
content.body = "\(task)!!"
content.sound = UNNotificationSound.default()
content.categoryIdentifier = Identifiers.reminderCategory複製代碼
咱們使用一個 UNMutableNotificationContent
對象來配置 notification 的外觀和內容。設好 title 和 content,這是後面用戶在通知 banner 裏看到的標題和內容。另外,咱們指定了通知出現時播放的聲音爲默認聲音。固然你也能夠指定一個本身想要的聲音。
最後,咱們設置 categoryIdentifier
,待會爲通知添加自定義操做的時候會用到。
// 咱們但願能每 60 秒提醒咱們一次 (這也是蘋果容許的最小通知間隔)
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 60, repeats: true)複製代碼
通知中心會根據這個 trigger 來決定何時展現通知。若是沒提供 trigger,通知就會當即發出去。
有幾種不一樣的 trigger:
UNTimeIntervalNotificationTrigger
: 能讓通知在一段指定長度的時間間隔後發出。若是須要,後面能夠按這個時間間隔週期性重複通知。UNCalendarNotificationTrigger
: 在特定的時刻進行通知,例如:早上 8 點通知。也能夠週期重複。UNLocationNotificationTrigger
: 在用戶進入/離開某個地點的時候進行通知。對咱們目前的需求而言,咱們選擇 UNTimeIntervalNotificationTrigger
,設定爲每分鐘重複一次。
let identifier = "\(task)"複製代碼
咱們的 app 能讓用戶爲 tasks
數組裏的每一項 task 添加通知。而這個 identifier
能讓咱們(沒錯,你猜對了)肯定跟通知相關聯的是哪一項 task。
// 用上面寫好的部分來組建一個 request
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)複製代碼
使用上面講過的 identifier、content、trigger,咱們建立了一個 UNNotificationRequest
對象,它含有通知所需的全部信息。咱們再把這個對象傳給通知中心:
UNUserNotificationCenter.current().add(request) {
error in
if let error = error {
print("Problem adding notification: \(error.localizedDescription)")
}
else {
// 設置喇叭圖標
DispatchQueue.main.async {
if let cell = self.cell(for: task) {
cell.showReminderOnIcon()
}
}
}
}
}複製代碼
若是添加通知沒有問題,咱們就更新那個 task 對應的 cell 上顯示的喇叭圖標,表示提醒已經打開了。注意 UI 操做須要回到主線程來進行,這是由於添加通知的 completion block 是在後臺線程上調用的。
上面提到過,咱們寫了一個 retrieveNotification
方法來取消以前預定的通知。使用新的通知 API 實現這個功能很是簡單:
func retrieveNotification(for task: Task, completion: @escaping (UNNotificationRequest?) -> ()) {
UNUserNotificationCenter.current().getPendingNotificationRequests {
requests in
DispatchQueue.main.async {
let request = requests.filter { $0.identifier == task }.first
completion(request)
}
}
}複製代碼
爲了照顧到以前寫的 completion block,咱們要把回調切回主線程。
前面配置 tableViewCell 的時候,用過一個 toggleReminder
方法,來爲點擊的 task 添加或移除通知提醒。下面咱們實現這個方法:
func toggleReminder(for task: Task) {
retrieveNotification(for: task) {
request in
guard request != nil else {
// 以前並無通知,因此該添加通知
self.createReminderNotification(for: task)
return
}
// 移除通知
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [task])
// 咱們已經把通知取消了,下面更新 cell 上的喇叭圖標來顯示這一點
if let cell = self.cell(for: task) {
cell.showReminderOffIcon()
}
}
}複製代碼
若是 request
是 nil,說明以前沒有設置通知,所以咱們就設置一個。不然,就把 task 的 identifier (例如 「鍛鍊」或者「遛狗」)傳給通知中心,移除以前的通知;以後更新 cell 上的喇叭圖標,表示通知已經被禁了。
大功告成!如今咱們有了一個每 60 秒提醒一次的通知,直到用戶回到 app 裏、找到對應的 task ,把提醒關掉纔會中止。
然而,若是用戶能在通知彈出時直接關掉後續的提醒,就更好了……
咱們能夠給通知添加操做來實現這個功能。用戶在通知的 banner 下劃,或者在鎖屏界面的通知上左劃,都能看到能夠點擊的 action 按鈕。
最多能夠增長 4 種操做(雖然蘋果表示在某些設備上只能顯示前兩種操做,由於屏幕空間過小),一種操做就是一個「category」。
func addCategory() {
// 添加操做
let cancelAction = UNNotificationAction(identifier: Identifiers.cancelAction,
title: "Cancel",
options: [.foreground])
// 建立 category
let category = UNNotificationCategory(identifier: Identifiers.reminderCategory,
actions: [cancelAction],
intentIdentifiers: [],
options: [])
UNUserNotificationCenter.current().setNotificationCategories([category])
}複製代碼
咱們把 action 的選項設置爲UNNotificationActionOptions
的 .foreground
,意思是點擊 action 按鈕時會把應用打開到前臺。其餘可用的選項包括能夠表示這項操做要謹慎進行(例如刪除類操做),或者在執行前要先解鎖。咱們在 application(_:didFinishLaunchingWithOptions:)
裏調用 addCategory()
方法。
如今 identifier
只是簡單的字符串,一旦拼錯幾個字母就無法正常工做了。我曾經一邊寫成了 "cancel"、另外一邊寫成了 "Cancel",花了好一下子才排查出來。因此我以爲應該寫一個簡單的結構體,安放全部 identifier。
struct Identifiers {
static let reminderCategory = "reminder"
static let cancelAction = "cancel"
}複製代碼
爲了處理通知 banner 被點擊的事件,咱們須要實現 UNUserNotificationCenterDelegate
接口。爲簡潔起見,咱們就讓 AppDelegate
來當處理事件的 delegate,在 application(_:didFinishLaunchingWithOptions:) :
裏設置:
UNUserNotificationCenter.current().delegate = self複製代碼
而後咱們來實現點擊事件:
public func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
if response.actionIdentifier == Identifiers.cancelAction {
let request = response.notification.request
print("Removing item with identifier \(request.identifier)")
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [request.identifier])
}
completionHandler()
}複製代碼
首先判斷這個方法的調用來源是用戶點擊了通知上的 action 按鈕(也多是用戶直接點擊通知調用的,這種狀況下咱們不進行任何處理)。若是是,那麼咱們就直接移除 identifier 對應的通知。
最後調用 completionHandler
來通知系統咱們已經處理完成,它能夠該幹什麼幹什麼去了。
好,咱們快說完了。可是若是咱們的 app 正在前臺的時候,通知就來了,會怎麼辦呢?若是不作任何處理的話,通知就會被系統默認丟棄了。咱們簡單改一下吧。
這是 iOS 10 新加的一個頗有用的功能:你能夠選擇當 app 在前臺時是否顯示通知。只需實現 delegate 方法,添加一句代碼:
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler(.alert)
}複製代碼
上面的寫法就是告訴系統,應該用 alert 顯示通知。
本文介紹了新的 UserNotifications
框架在預定本地通知方面的強大功能。看起來蘋果終於聽取了開發者的抱怨,推出了可讀易用的 API。
雖然咱們沒有篇幅詳細探討遠程推送的通知,新的框架在這方面也有所改進,它讓本地和遠程推送的通知能用相同的 API 統一處理,所以減小了代碼冗餘。
要了解更多,能夠觀看 WWDC 2016 的視頻 Introduction to Notifications。同時,歡迎來戳咱們在 Github 上的樣例工程。
原文地址:iOS 10 Day by Day :: Day 5 :: User Notifications
原做者:Sam Burnstone @sam_burnstone
ShinobiControls 官網:ShinobiControls.com twitter : @shinobicontrols
譯者:戴倉薯