iOS 10 最重要的變化可能就是通知 API 的重構(gòu)了牙躺。本文用一個(gè)簡(jiǎn)單鬧鐘的例子介紹了 User Notification 的 API 變化和新功能腺办。
《iOS 10 day by day》是 shinobicontrols 公司編寫(xiě)的系列博客,介紹開(kāi)發(fā)者需要了解的 iOS 10 新特性逆瑞,每周更新荠藤。本系列翻譯(文集地址)已取得官方授權(quán)伙单。目錄點(diǎn)此。倉(cāng)薯翻譯哈肖,歡迎指正:)
Shinobicontrols 為 iOS 和 Android 開(kāi)發(fā)者提供高性能吻育、響應(yīng)式的 UI 控件 SDK,尤其是圖表方面的控件淤井。 官網(wǎng) : shinobicontrols.com twitter : @shinobicontrols
簡(jiǎn)介
很久以前布疼,開(kāi)發(fā)者就可以在 iOS 里預(yù)約本地通知了,但是之前的 API 缺乏細(xì)粒度的控制能力币狠。幸運(yùn)的是游两,蘋(píng)果在 iOS 10 中改善了這一點(diǎn),發(fā)布了新的 UserNotifications
框架漩绵。這個(gè)框架在處理本地通知及遠(yuǎn)程推送方面的 API 豐富了許多贱案,同時(shí)寫(xiě)法更加簡(jiǎn)便。
本地通知(local notification)是用 app 來(lái)預(yù)約的通知止吐,例如:提醒你帶午飯的鬧鐘轰坊。而遠(yuǎn)程推送(remote notification)一般是服務(wù)器發(fā)起的,傳到蘋(píng)果的 APNS 服務(wù)器上祟印,APNS 再推送到用戶(hù)手機(jī)上肴沫。例如:推送給所有用戶(hù),告訴他們 app 發(fā)布新版本了蕴忆。
實(shí)例工程
工程是用 Xcode 8 Beta 6 建的
我們用一個(gè)簡(jiǎn)單的鬧鐘 app 來(lái)介紹新的 UserNotification
框架颤芬,一個(gè)用戶(hù)可以預(yù)約提醒的 to do list。到時(shí)間后套鹅,鬧鐘每 60 秒提醒一次站蝠,直到用戶(hù)手動(dòng)取消為止。跟之前一樣卓鹿,代碼放在 github 上菱魔。
每個(gè)小喇叭的圖標(biāo)表示一個(gè)預(yù)約好的提醒,而被紅色斜杠劃掉的小喇叭表示這個(gè)事項(xiàng)不需要提醒吟孙。
我們還會(huì)添加讓用戶(hù)對(duì)通知做出響應(yīng)的功能:
UI 部分
UI 界面上就是一個(gè)簡(jiǎn)單的 tableView澜倦,顯示用戶(hù)的 to do list。沒(méi)什么可說(shuō)的杰妓。
提醒事項(xiàng)的數(shù)據(jù)類(lèi)型是這樣定義的:
class NagMeTableViewController: UITableViewController {
typealias Task = String
let tasks: [Task] = [
"Wash Up",
"Walk Dog",
"Exercise"
]
// 待續(xù)
我們的 tableView 就是一個(gè)提醒事項(xiàng)的列表藻治,點(diǎn)擊 cell 上的小喇叭按鈕會(huì)調(diào)用一個(gè)閉包。
// 續(xù)上
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 上提醒/不提醒的圖標(biāo)
retrieveNotification(for: task) {
request in
request != nil ? cell.showReminderOnIcon() : cell.showReminderOffIcon()
}
// 點(diǎn)擊按鈕時(shí)調(diào)用閉包
cell.onButtonSelection = {
[unowned self] in
self.toggleReminder(for: task)
}
return cell
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tasks.count
}
}
為了判斷用戶(hù)是不是當(dāng)前『正在被提醒』巷挥,我們要調(diào)一個(gè) retrieveNotification(for: task)
方法桩卵,待會(huì)再詳細(xì)說(shuō)。如果存在 notification 對(duì)象,說(shuō)明用戶(hù)要求提醒這個(gè)事項(xiàng)雏节。
當(dāng)點(diǎn)擊 cell 上喇叭按鈕的時(shí)候胜嗓,會(huì)調(diào)用一個(gè) toggleReminder(for: task)
方法,我們也放在后文介紹钩乍。這個(gè)方法里就是預(yù)約提醒的神奇魔法兼蕊。
請(qǐng)求用戶(hù)授權(quán)
在預(yù)約提醒之前,需要先向用戶(hù)請(qǐng)求通知的授權(quán)件蚕。在 app 啟動(dòng)時(shí)調(diào)用如下代碼:
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")
}
}
}
調(diào)用的結(jié)果是會(huì)顯示一個(gè)彈窗孙技,詢(xún)問(wèn)用戶(hù)是否允許我們的 app 發(fā)送通知。閉包的 granted
參數(shù)表示我們是否取到了權(quán)限排作。這個(gè)彈窗只會(huì)顯示一次牵啦,不過(guò)之后用戶(hù)也可以在設(shè)置里進(jìn)行更改。
你會(huì)發(fā)現(xiàn)妄痪,User Notification
框架大量的 API 使用了 completion block哈雏。這是因?yàn)橄?UNUserNotificationCenter
發(fā)出的請(qǐng)求大部分都是在后臺(tái)線程上異步執(zhí)行的。調(diào)用 current()
方法會(huì)讓框架返回一個(gè)供我們 app 使用的 notification center 單例對(duì)象衫生,而我們所有的預(yù)約通知裳瘪、取消通知都要通過(guò)這個(gè)單例對(duì)象來(lái)實(shí)現(xiàn)。
創(chuàng)建通知
創(chuàng)建罪针、添加通知的過(guò)程實(shí)在有些冗長(zhǎng)彭羹,我們把代碼分解成幾部分,一步一步來(lái)看:
/// 為 task 創(chuàng)建一個(gè) notification泪酱,每分鐘重復(fù)一次
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
我們使用一個(gè) UNMutableNotificationContent
對(duì)象來(lái)配置 notification 的外觀和內(nèi)容派殷。設(shè)好 title 和 content,這是后面用戶(hù)在通知 banner 里看到的標(biāo)題和內(nèi)容墓阀。另外毡惜,我們指定了通知出現(xiàn)時(shí)播放的聲音為默認(rèn)聲音。當(dāng)然你也可以指定一個(gè)自己想要的聲音斯撮。
最后经伙,我們?cè)O(shè)置 categoryIdentifier
,待會(huì)為通知添加自定義操作的時(shí)候會(huì)用到勿锅。
// 我們希望能每 60 秒提醒我們一次 (這也是蘋(píng)果允許的最小通知間隔)
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 60, repeats: true)
通知中心會(huì)根據(jù)這個(gè) trigger 來(lái)決定什么時(shí)候展示通知帕膜。如果沒(méi)提供 trigger,通知就會(huì)立即發(fā)出去粱甫。
有幾種不同的 trigger:
-
UNTimeIntervalNotificationTrigger
: 能讓通知在一段指定長(zhǎng)度的時(shí)間間隔后發(fā)出泳叠。如果需要,后面可以按這個(gè)時(shí)間間隔周期性重復(fù)通知茶宵。 -
UNCalendarNotificationTrigger
: 在特定的時(shí)刻進(jìn)行通知,例如:早上 8 點(diǎn)通知宗挥。也可以周期重復(fù)乌庶。 -
UNLocationNotificationTrigger
: 在用戶(hù)進(jìn)入/離開(kāi)某個(gè)地點(diǎn)的時(shí)候進(jìn)行通知种蝶。
對(duì)我們目前的需求而言,我們選擇 UNTimeIntervalNotificationTrigger
瞒大,設(shè)定為每分鐘重復(fù)一次螃征。
let identifier = "\(task)"
我們的 app 能讓用戶(hù)為 tasks
數(shù)組里的每一項(xiàng) task 添加通知。而這個(gè) identifier
能讓我們(沒(méi)錯(cuò)透敌,你猜對(duì)了)確定跟通知相關(guān)聯(lián)的是哪一項(xiàng) task盯滚。
// 用上面寫(xiě)好的部分來(lái)組建一個(gè) request
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
使用上面講過(guò)的 identifier、content酗电、trigger魄藕,我們創(chuàng)建了一個(gè) UNNotificationRequest
對(duì)象,它含有通知所需的所有信息撵术。我們?cè)侔堰@個(gè)對(duì)象傳給通知中心:
UNUserNotificationCenter.current().add(request) {
error in
if let error = error {
print("Problem adding notification: \(error.localizedDescription)")
}
else {
// 設(shè)置喇叭圖標(biāo)
DispatchQueue.main.async {
if let cell = self.cell(for: task) {
cell.showReminderOnIcon()
}
}
}
}
}
如果添加通知沒(méi)有問(wèn)題背率,我們就更新那個(gè) task 對(duì)應(yīng)的 cell 上顯示的喇叭圖標(biāo),表示提醒已經(jīng)打開(kāi)了嫩与。注意 UI 操作需要回到主線程來(lái)進(jìn)行寝姿,這是因?yàn)樘砑油ㄖ?completion block 是在后臺(tái)線程上調(diào)用的。
取消通知
上面提到過(guò)划滋,我們寫(xiě)了一個(gè) retrieveNotification
方法來(lái)取消之前預(yù)約的通知饵筑。使用新的通知 API 實(shí)現(xiàn)這個(gè)功能非常簡(jiǎn)單:
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)
}
}
}
為了照顧到之前寫(xiě)的 completion block,我們要把回調(diào)切回主線程处坪。
把通知操作與界面關(guān)聯(lián)起來(lái)
前面配置 tableViewCell 的時(shí)候翻翩,用過(guò)一個(gè) toggleReminder
方法,來(lái)為點(diǎn)擊的 task 添加或移除通知提醒稻薇。下面我們實(shí)現(xiàn)這個(gè)方法:
func toggleReminder(for task: Task) {
retrieveNotification(for: task) {
request in
guard request != nil else {
// 之前并沒(méi)有通知嫂冻,所以該添加通知
self.createReminderNotification(for: task)
return
}
// 移除通知
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [task])
// 我們已經(jīng)把通知取消了,下面更新 cell 上的喇叭圖標(biāo)來(lái)顯示這一點(diǎn)
if let cell = self.cell(for: task) {
cell.showReminderOffIcon()
}
}
}
如果 request
是 nil塞椎,說(shuō)明之前沒(méi)有設(shè)置通知桨仿,因此我們就設(shè)置一個(gè)。否則案狠,就把 task 的 identifier (例如 “鍛煉”或者“遛狗”)傳給通知中心服傍,移除之前的通知;之后更新 cell 上的喇叭圖標(biāo)骂铁,表示通知已經(jīng)被禁了吹零。
大功告成!現(xiàn)在我們有了一個(gè)每 60 秒提醒一次的通知拉庵,直到用戶(hù)回到 app 里灿椅、找到對(duì)應(yīng)的 task ,把提醒關(guān)掉才會(huì)停止。
然而茫蛹,如果用戶(hù)能在通知彈出時(shí)直接關(guān)掉后續(xù)的提醒操刀,就更好了……
添加通知的操作
我們可以給通知添加操作來(lái)實(shí)現(xiàn)這個(gè)功能。用戶(hù)在通知的 banner 下劃婴洼,或者在鎖屏界面的通知上左劃骨坑,都能看到可以點(diǎn)擊的 action 按鈕。
最多可以增加 4 種操作(雖然蘋(píng)果表示在某些設(shè)備上只能顯示前兩種操作柬采,因?yàn)槠聊豢臻g太谢锻佟),一種操作就是一個(gè)“category”粉捻。
func addCategory() {
// 添加操作
let cancelAction = UNNotificationAction(identifier: Identifiers.cancelAction,
title: "Cancel",
options: [.foreground])
// 創(chuàng)建 category
let category = UNNotificationCategory(identifier: Identifiers.reminderCategory,
actions: [cancelAction],
intentIdentifiers: [],
options: [])
UNUserNotificationCenter.current().setNotificationCategories([category])
}
我們把 action 的選項(xiàng)設(shè)置為UNNotificationActionOptions
的 .foreground
礁遣,意思是點(diǎn)擊 action 按鈕時(shí)會(huì)把應(yīng)用打開(kāi)到前臺(tái)。其他可用的選項(xiàng)包括可以表示這項(xiàng)操作要謹(jǐn)慎進(jìn)行(例如刪除類(lèi)操作)杀迹,或者在執(zhí)行前要先解鎖亡脸。我們?cè)?application(_:didFinishLaunchingWithOptions:)
里調(diào)用 addCategory()
方法。
現(xiàn)在 identifier
只是簡(jiǎn)單的字符串树酪,一旦拼錯(cuò)幾個(gè)字母就沒(méi)法正常工作了浅碾。我曾經(jīng)一邊寫(xiě)成了 "cancel"、另一邊寫(xiě)成了 "Cancel"续语,花了好一會(huì)兒才排查出來(lái)垂谢。所以我覺(jué)得應(yīng)該寫(xiě)一個(gè)簡(jiǎn)單的結(jié)構(gòu)體,安放所有 identifier疮茄。
struct Identifiers {
static let reminderCategory = "reminder"
static let cancelAction = "cancel"
}
為了處理通知 banner 被點(diǎn)擊的事件滥朱,我們需要實(shí)現(xiàn) UNUserNotificationCenterDelegate
接口。為簡(jiǎn)潔起見(jiàn)力试,我們就讓 AppDelegate
來(lái)當(dāng)處理事件的 delegate徙邻,在 application(_:didFinishLaunchingWithOptions:) :
里設(shè)置:
UNUserNotificationCenter.current().delegate = self
然后我們來(lái)實(shí)現(xiàn)點(diǎn)擊事件:
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()
}
首先判斷這個(gè)方法的調(diào)用來(lái)源是用戶(hù)點(diǎn)擊了通知上的 action 按鈕(也可能是用戶(hù)直接點(diǎn)擊通知調(diào)用的,這種情況下我們不進(jìn)行任何處理)畸裳。如果是缰犁,那么我們就直接移除 identifier 對(duì)應(yīng)的通知。
最后調(diào)用 completionHandler
來(lái)通知系統(tǒng)我們已經(jīng)處理完成怖糊,它可以該干什么干什么去了帅容。
好,我們快說(shuō)完了伍伤。但是如果我們的 app 正在前臺(tái)的時(shí)候并徘,通知就來(lái)了,會(huì)怎么辦呢扰魂?如果不做任何處理的話麦乞,通知就會(huì)被系統(tǒng)默認(rèn)丟棄了蕴茴。我們簡(jiǎn)單改一下吧。
當(dāng) app 在前臺(tái)時(shí)接收通知
這是 iOS 10 新加的一個(gè)很有用的功能:你可以選擇當(dāng) app 在前臺(tái)時(shí)是否顯示通知路幸。只需實(shí)現(xiàn) delegate 方法荐开,添加一句代碼:
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler(.alert)
}
上面的寫(xiě)法就是告訴系統(tǒng)付翁,應(yīng)該用 alert 顯示通知简肴。
擴(kuò)展閱讀
本文介紹了新的 UserNotifications
框架在預(yù)約本地通知方面的強(qiáng)大功能“俨啵看起來(lái)蘋(píng)果終于聽(tīng)取了開(kāi)發(fā)者的抱怨砰识,推出了可讀易用的 API。
雖然我們沒(méi)有篇幅詳細(xì)探討遠(yuǎn)程推送的通知佣渴,新的框架在這方面也有所改進(jìn)辫狼,它讓本地和遠(yuǎn)程推送的通知能用相同的 API 統(tǒng)一處理,因此減少了代碼冗余辛润。
要了解更多膨处,可以觀看 WWDC 2016 的視頻 Introduction to Notifications。同時(shí)砂竖,歡迎來(lái)戳我們?cè)? Github 上的樣例工程真椿。
原作者:Sam Burnstone @sam_burnstone
ShinobiControls 官網(wǎng):ShinobiControls.com twitter : @shinobicontrols
譯者:戴倉(cāng)薯