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
译者:戴仓薯