[转载]iOS 10 UserNotifications 框架解析

活久见的重构 - iOS 10 UserNotifications 框架解析

TL;DR

iOS 10 中之前杂乱的和通知相关的 API 都被统一了,如今开发者可使用独立的 UserNotifications.framework 来集中管理和使用 iOS 系统中通知的功能。在此基础上,Apple 还增长了撤回单条通知,更新已展现通知,中途修改通知内容,在通知中展现图片视频,自定义通知 UI 等一系列新功能,很是强大。html

对于开发者来讲,相较于以前版本,iOS 10 提供了一套很是易用通知处理接口,是 SDK 的一次重大重构。而以前的绝大部分通知相关 API 都已经被标为弃用 (deprecated)。ios

这篇文章将首先回顾一下 Notification 的发展历史和现状,而后经过一些例子来展现 iOS 10 SDK 中相应的使用方式,来讲明新 SDK 中通知能够作的事情以及它们的使用方式。git

您能够在 WWDC 16 的 Introduction to Notifications 和 Advanced Notifications 这两个 Session 中找到详细信息;另外也不要忘了参照UserNotifications 的官方文档以及本文的实例项目 UserNotificationDemogithub

Notification 历史和现状

碎片化时间是移动设备用户在使用应用时的一大特色,用户但愿随时拿起手机就能查看资讯,处理事务,而通知能够在重要的事件和信息发生时提醒用户。完美的通知展现能够很好地帮助用户使用应用,体现出应用的价值,进而有很大可能将用户带回应用,提升活跃度。正因如此,不管是 Apple 仍是第三方开发者们,都很重视通知相关的开发工做,而通知也成为了不少应用的必备功能,开发者们都但愿通知能带来更好地体验和更多的用户。web

可是理想的丰满并不能弥补现实的骨感。自从在 iOS 3 引入 Push Notification 后,以后几乎每一个版本 Apple 都在增强这方面的功能。咱们能够回顾一下整个历程和相关的主要 API:json

  • iOS 3 - 引入推送通知 UIApplication 的registerForRemoteNotificationTypes 与 UIApplicationDelegate 的application(_:didRegisterForRemoteNotificationsWithDeviceToken:)application(_:didReceiveRemoteNotification:)
  • iOS 4 - 引入本地通知scheduleLocalNotificationpresentLocalNotificationNow:application(_:didReceive:)
  • iOS 5 - 加入通知中心页面
  • iOS 6 - 通知中心页面与 iCloud 同步
  • iOS 7 - 后台静默推送application(_:didReceiveRemoteNotification:fetchCompletionHandle:)
  • iOS 8 - 从新设计 notification 权限请求,Actionable 通知registerUserNotificationSettings(_:)UIUserNotificationActionUIUserNotificationCategoryapplication(_:handleActionWithIdentifier:forRemoteNotification:completionHandler:) 等
  • iOS 9 - Text Input action,基于 HTTP/2 的推送请求UIUserNotificationActionBehavior,全新的 Provider API 等

有点晕,不是么?一个开发者很难在不借助于文档的帮助下区分application(_:didReceiveRemoteNotification:) 和application(_:didReceiveRemoteNotification:fetchCompletionHandle:),新入行的开发者也不可能明白 registerForRemoteNotificationTypes 和registerUserNotificationSettings(_:) 之间是否是有什么关系,Remote 和 Local Notification 除了在初始化方式以外那些细微的区别也让人抓狂,而不少 API 都被随意地放在了 UIApplication 或者 UIApplicationDelegate中。除此以外,应用已经在前台时,远程推送是没法直接显示的,要先捕获到远程来的通知,而后再发起一个本地通知才能完成现实。更让人郁闷的是,应用在运行时和非运行时捕获通知的路径还不一致。虽然这些种种问题都是由必定历史缘由形成的,但不能否认,正是混乱的组织方式和以前版本的考虑不周,使得 iOS 通知方面的开发一直称不上“让人愉悦”,甚至有很多“坏代码”的味道。swift

另外一方面,如今的通知功能相对仍是简单,咱们能作的只是本地或者远程发起通知,而后显示给用户。虽然 iOS 8 和 9 中添加了按钮和文原本进行交互,可是已发出的通知不能更新,通知的内容也只是在发起时惟一肯定,而这些内容也只能是简单的文本。 想要在现有基础上扩展通知的功能,势必会让本来就盘根错节的 API 更加难以理解。api

在 iOS 10 中新加入 UserNotifications 框架,能够说是 iOS SDK 发展到如今的最大规模的一次重构。新版本里通知的相关功能被提取到了单独的框架,通知也再也不区分类型,而有了更统一的行为。咱们接下来就将由浅入深地解析这个重构后的框架的使用方式。数组

UserNotifications 框架解析

基本流程

iOS 10 中通知相关的操做遵循下面的流程:缓存

首先你须要向用户请求推送权限,而后发送通知。对于发送出的通知,若是你的应用位于后台或者没有运行的话,系统将经过用户容许的方式 (弹窗,横幅,或者是在通知中心) 进行显示。若是你的应用已经位于前台正在运行,你能够自行决定要不要显示这个通知。最后,若是你但愿用户点击通知能有打开应用之外的额外功能的话,你也须要进行处理。

权限申请

通用权限

iOS 8 以前,本地推送 (UILocalNotification) 和远程推送 (Remote Notification) 是区分对待的,应用只须要在进行远程推送时获取用户赞成。iOS 8 对这一行为进行了规范,由于不管是本地推送仍是远程推送,其实在用户看来表现是一致的,都是打断用户的行为。所以从 iOS 8 开始,这两种通知都须要申请权限。iOS 10 里进一步消除了本地通知和推送通知的区别。向用户申请通知权限很是简单:

UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in if granted { // 用户容许进行通知 } } 

固然,在使用 UN 开头的 API 的时候,不要忘记导入 UserNotifications 框架:

import UserNotifications 

第一次调用这个方法时,会弹出一个系统弹窗。

要注意的是,一旦用户拒绝了这个请求,再次调用该方法也不会再进行弹窗,想要应用有机会接收到通知的话,用户必须自行前往系统的设置中为你的应用打开通知,而这每每是不可能的。所以,在合适的时候弹出请求窗,在请求权限前预先进行说明,而不是直接粗暴地在启动的时候就进行弹窗,会是更明智的选择。

远程推送

一旦用户赞成后,你就能够在应用中发送本地通知了。不过若是你经过服务器发送远程通知的话,还须要多一个获取用户 token 的操做。你的服务器可使用这个 token 将用向 Apple Push Notification 的服务器提交请求,而后 APNs 经过 token 识别设备和应用,将通知推给用户。

提交 token 请求和得到 token 的回调是如今“惟二”不在新框架中的 API。咱们使用 UIApplication 的 registerForRemoteNotifications 来注册远程通知,在 AppDelegate 的application(_:didRegisterForRemoteNotificationsWithDeviceToken) 中获取用户 token:

// 向 APNs 请求 token: UIApplication.shared.registerForRemoteNotifications() // AppDelegate.swift func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let tokenString = deviceToken.hexString print("Get Push token: \(tokenString)") } 

获取获得的 deviceToken 是一个 Data 类型,为了方便使用和传递,咱们通常会选择将它转换为一个字符串。Swift 3 中可使用下面的 Data 扩展来构造出适合传递给 Apple 的字符串:

extension Data { var hexString: String { return withUnsafeBytes {(bytes: UnsafePointer<UInt8>) -> String in let buffer = UnsafeBufferPointer(start: bytes, count: count) return buffer.map {String(format: "%02hhx", $0)}.reduce("", { $0 + $1 }) } } } 

权限设置

用户能够在系统设置中修改你的应用的通知权限,除了打开和关闭所有通知权限外,用户也能够限制你的应用只能进行某种形式的通知显示,好比只容许横幅而不容许弹窗及通知中心显示等。通常来讲你不该该对用户的选择进行干涉,可是若是你的应用确实须要某种特定场景的推送的话,你能够对当前用户进行的设置进行检查:

UNUserNotificationCenter.current().getNotificationSettings {
    settings in 
    print(settings.authorizationStatus) // .authorized | .denied | .notDetermined
    print(settings.badgeSetting) // .enabled | .disabled | .notSupported
    // etc...
}

关于权限方面的使用,能够参考 Demo 中AuthorizationViewController 的内容。

发送通知

UserNotifications 中对通知进行了统一。咱们经过通知的内容 (UNNotificationContent),发送的时机 (UNNotificationTrigger) 以及一个发送通知的 String 类型的标识符,来生成一个 UNNotificationRequest 类型的发送请求。最后,咱们将这个请求添加到UNUserNotificationCenter.current() 中,就能够等待通知到达了:

// 1. 建立通知内容 let content = UNMutableNotificationContent() content.title = "Time Interval Notification" content.body = "My first notification" // 2. 建立发送触发 let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false) // 3. 发送请求标识符 let requestIdentifier = "com.onevcat.usernotification.myFirstNotification" // 4. 建立一个发送请求 let request = UNNotificationRequest(identifier: requestIdentifier, content: content, trigger: trigger) // 将请求添加到发送中心 UNUserNotificationCenter.current().add(request) { error in if error == nil { print("Time Interval Notification scheduled: \(requestIdentifier)") } } 
  1. iOS 10 中通知不只支持简单的一行文字,你还能够添加 title 和subtitle,来用粗体字的形式强调通知的目的。对于远程推送,iOS 10 以前通常只含有消息的推送 payload 是这样的:

    {
      "aps":{ "alert":"Test", "sound":"default", "badge":1 } } 

    若是咱们想要加入 title 和 subtitle 的话,则须要将 alert 从字符串换为字典,新的 payload 是:

    {
      "aps":{ "alert":{ "title":"I am title", "subtitle":"I am subtitle", "body":"I am body" }, "sound":"default", "badge":1 } } 

    好消息是,后一种字典的方法其实在 iOS 8.2 的时候就已经存在了。虽然当时 title 只是用在 Apple Watch 上的,可是设置好 body 的话在 iOS 上仍是能够显示的,因此针对 iOS 10 添加标题时是能够保证前向兼容的。

    另外,若是要进行本地化对应,在设置这些内容文本时,本地可使用String.localizedUserNotificationString(forKey: "your_key", arguments: []) 的方式来从 Localizable.strings 文件中取出本地化字符串,而远程推送的话,也能够在 payload 的 alert 中使用 loc-key或者 title-loc-key 来进行指定。关于 payload 中的 key,能够参考这篇文档

  2. 触发器是只对本地通知而言的,远程推送的通知的话默认会在收到后当即显示。如今 UserNotifications 框架中提供了三种触发器,分别是:在必定时间后触发 UNTimeIntervalNotificationTrigger,在某月某日某时触发 UNCalendarNotificationTrigger 以及在用户进入或是离开某个区域时触发 UNLocationNotificationTrigger

  3. 请求标识符能够用来区分不一样的通知请求,在将一个通知请求提交后,经过特定 API 咱们可以使用这个标识符来取消或者更新这个通知。咱们将在稍后再提到具体用法。

  4. 在新版本的通知框架中,Apple 借用了一部分网络请求的概念。咱们组织并发送一个通知请求,而后将这个请求提交给UNUserNotificationCenter 进行处理。咱们会在 delegate 中接收到这个通知请求对应的 response,另外咱们也有机会在应用的 extension 中对 request 进行处理。咱们在接下来的章节会看到更多这方面的内容。

在提交通知请求后,咱们锁屏或者将应用切到后台,并等待设定的时间后,就能看到咱们的通知出如今通知中心或者屏幕横幅了:

关于最基础的通知发送,能够参考 Demo 中TimeIntervalViewController 的内容。

取消和更新

在建立通知请求时,咱们已经指定了标识符。这个标识符能够用来管理通知。在 iOS 10 以前,咱们很难取消掉某一个特定的通知,也不能主动移除或者更新已经展现的通知。想象一下你须要推送用户帐户内的余额变化状况,屡次的余额增减或者变化很容易让用户十分困惑 - 到底哪条通知才是最正确的?又或者在推送一场比赛的比分时,频繁的通知必然致使用户通知中心数量爆炸,而大部分中途的比分对于用户来讲只是噪音。

iOS 10 中,UserNotifications 框架提供了一系列管理通知的 API,你能够作到:

  • 取消还未展现的通知
  • 更新还未展现的通知
  • 移除已经展现过的通知
  • 更新已经展现过的通知

其中关键就在于在建立请求时使用一样的标识符。

好比,从通知中心中移除一个展现过的通知:

let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3, repeats: false) let identifier = "com.onevcat.usernotification.notificationWillBeRemovedAfterDisplayed" let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in if error != nil { print("Notification request added: \(identifier)") } } delay(4) { print("Notification request removed: \(identifier)") UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [identifier]) } 

相似地,咱们可使用 removePendingNotificationRequests,来取消还未展现的通知请求。对于更新通知,不管是否已经展现,都和一开始添加请求时同样,再次将请求提交给 UNUserNotificationCenter 便可:

// let request: UNNotificationRequest = ... UNUserNotificationCenter.current().add(request) { error in if error != nil { print("Notification request added: \(identifier)") } } delay(2) { let newTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) // Add new request with the same identifier to update a notification. let newRequest = UNNotificationRequest(identifier: identifier, content:newContent, trigger: newTrigger) UNUserNotificationCenter.current().add(newRequest) { error in if error != nil { print("Notification request updated: \(identifier)") } } } 

远程推送能够进行通知的更新,在使用 Provider API 向 APNs 提交请求时,在 HTTP/2 的 header 中 apns-collapse-id key 的内容将被做为该推送的标识符进行使用。屡次推送同一标识符的通知便可进行更新。

对应本地的 removeDeliveredNotifications,如今还不能经过相似的方式,向 APNs 发送一个包含 collapse id 的 DELETE 请求来删除已经展现的推送,APNs 服务器并不接受一个 DELETE 请求。不过从技术上来讲 Apple 方面应该不存在什么问题,咱们能够拭目以待。如今若是想要消除一个远程推送,能够选择使用后台静默推送的方式来从本地发起一个删除通知的调用。关于后台推送的部分,能够参考我以前的一篇关于 iOS7 中的多任务的文章。

关于通知管理,能够参考 Demo 中 ManagementViewController 的内容。为了可以简单地测试远程推送,通常咱们都会用一些方便发送通知的工具,Knuff 就是其中之一。我也为 Knuff 添加了 apns-collapse-id 的支持,你能够在这个 fork 的 repo 或者是原 repo 的 pull request 中找到相关信息。

处理通知

应用内展现通知

如今系统能够在应用处于后台或者退出的时候向用户展现通知了。不过,当应用处于前台时,收到的通知是没法进行展现的。若是咱们但愿在应用内也能显示通知的话,须要额外的工做。

UNUserNotificationCenterDelegate 提供了两个方法,分别对应如何在应用内展现通知,和收到通知响应时要如何处理的工做。咱们能够实现这个接口中的对应方法来在应用内展现通知:

class NotificationHandler: NSObject, UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert, .sound]) // 若是不想显示某个通知,能够直接用空 options 调用 completionHandler: // completionHandler([]) } } 

实现后,将 NotificationHandler 的实例赋值给UNUserNotificationCenter 的 delegate 属性就能够了。没有特殊理由的话,AppDelegate 的 application(_:didFinishLaunchingWithOptions:) 就是一个不错的选择:

class AppDelegate: UIResponder, UIApplicationDelegate { let notificationHandler = NotificationHandler() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { UNUserNotificationCenter.current().delegate = notificationHandler return true } } 

对通知进行响应

UNUserNotificationCenterDelegate 中还有一个方法,userNotificationCenter(_:didReceive:withCompletionHandler:)。这个代理方法会在用户与你推送的通知进行交互时被调用,包括用户经过通知打开了你的应用,或者点击或者触发了某个 action (咱们以后会提到 actionable 的通知)。由于涉及到打开应用的行为,因此实现了这个方法的 delegate 必须在 applicationDidFinishLaunching: 返回前就完成设置,这也是咱们以前推荐将 NotificationHandler 尽早进行赋值的理由。

一个最简单的实现天然是什么也不错,直接告诉系统你已经完成了全部工做。

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { completionHandler() } 

想让这个方法变得有趣一点的话,在建立通知的内容时,咱们能够在请求中附带一些信息:

let content = UNMutableNotificationContent() content.title = "Time Interval Notification" content.body = "My first notification" content.userInfo = ["name": "onevcat"] 

在该方法里,咱们将获取到这个推送请求对应的 response,UNNotificationResponse 是一个几乎包括了通知的全部信息的对象,从中咱们能够再次获取到 userInfo 中的信息:

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { if let name = response.notification.request.content.userInfo["name"] as? String { print("I know it's you! \(name)") } completionHandler() } 

更好的消息是,远程推送的 payload 内的内容也会出如今这个 userInfo中,这样一来,不管是本地推送仍是远程推送,处理的路径获得了统一。经过userInfo 的内容来决定页面跳转或者是进行其余操做,都会有很大空间。

Actionable 通知发送和处理

注册 Category

iOS 8 和 9 中 Apple 引入了能够交互的通知,这是经过将一簇 action 放到一个 category 中,将这个 category 进行注册,最后在发送通知时将通知的 category 设置为要使用的 category 来实现的。

注册一个 category 很是容易:

private func registerNotificationCategory() { let saySomethingCategory: UNNotificationCategory = { // 1 let inputAction = UNTextInputNotificationAction( identifier: "action.input", title: "Input", options: [.foreground], textInputButtonTitle: "Send", textInputPlaceholder: "What do you want to say...") // 2 let goodbyeAction = UNNotificationAction( identifier: "action.goodbye", title: "Goodbye", options: [.foreground]) let cancelAction = UNNotificationAction( identifier: "action.cancel", title: "Cancel", options: [.destructive]) // 3 return UNNotificationCategory(identifier:"saySomethingCategory", actions: [inputAction, goodbyeAction, cancelAction], intentIdentifiers: [], options: [.customDismissAction]) }() UNUserNotificationCenter.current().setNotificationCategories([saySomethingCategory]) } 
  1. UNTextInputNotificationAction 表明一个输入文本的 action,你能够自定义框的按钮 title 和 placeholder。你稍后会使用 identifier 来对 action 进行区分。
  2. 普通的 UNNotificationAction 对应标准的按钮。
  3. 为 category 指定一个 identifier,咱们将在实际发送通知的时候用这个标识符进行设置,这样系统就知道这个通知对应哪一个 category 了。

固然,不要忘了在程序启动时调用这个方法进行注册:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { registerNotificationCategory() UNUserNotificationCenter.current().delegate = notificationHandler return true } 

发送一个带有 action 的通知

在完成 category 注册后,发送一个 actionable 通知就很是简单了,只须要在建立 UNNotificationContent 时把 categoryIdentifier 设置为须要的 category id 便可:

content.categoryIdentifier = "saySomethingCategory" 

尝试展现这个通知,在下拉或者使用 3D touch 展开通知后,就能够看到对应的 action 了:

远程推送也可使用 category,只须要在 payload 中添加 category 字段,并指定预先定义的 category id 就能够了:

{
  "aps":{ "alert":"Please say something", "category":"saySomething" } } 

处理 actionable 通知

和普通的通知并没有二致,actionable 通知也会走到 didReceive 的 delegate 方法,咱们经过 request 中包含的 categoryIdentifier 和 response 里的actionIdentifier 就能够轻易断定是哪一个通知的哪一个操做被执行了。对于UNTextInputNotificationAction 触发的 response,直接将它转换为一个UNTextInputNotificationResponse,就能够拿到其中的用户输入了:

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { if let category = UserNotificationCategoryType(rawValue: response.notification.request.content.categoryIdentifier) { switch category { case .saySomething: handleSaySomthing(response: response) } } completionHandler() } private func handleSaySomthing(response: UNNotificationResponse) { let text: String if let actionType = SaySomethingCategoryAction(rawValue: response.actionIdentifier) { switch actionType { case .input: text = (response as! UNTextInputNotificationResponse).userText case .goodbye: text = "Goodbye" case .none: text = "" } } else { // Only tap or clear. (You will not receive this callback when user clear your notification unless you set .customDismissAction as the option of category) text = "" } if !text.isEmpty { UIAlertController.showConfirmAlertFromTopViewController(message: "You just said \(text)") } } 

上面的代码先判断通知响应是否属于 "saySomething",而后从用户输入或者是选择中提取字符串,而且弹出一个 alert 做为响应结果。固然,更多的状况下咱们会发送一个网络请求,或者是根据用户操做更新一些 UI 等。

关于 Actionable 的通知,能够参考 Demo 中ActionableViewController 的内容。

Notification Extension

iOS 10 中添加了不少 extension,做为应用与系统整合的入口。与通知相关的 extension 有两个:Service Extension 和 Content Extension。前者可让咱们有机会在收到远程推送的通知后,展现以前对通知内容进行修改;后者能够用来自定义通知视图的样式。

截取并修改通知内容

NotificationService 的模板已经为咱们进行了基本的实现:

class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? // 1 override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) if let bestAttemptContent = bestAttemptContent { if request.identifier == "mutableContent" { bestAttemptContent.body = "\(bestAttemptContent.body), onevcat" } contentHandler(bestAttemptContent) } } // 2 override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } } 
  1. didReceive: 方法中有一个等待发送的通知请求,咱们经过修改这个请求中的 content 内容,而后在限制的时间内将修改后的内容调用经过contentHandler 返还给系统,就能够显示这个修改过的通知了。
  2. 在必定时间内没有调用 contentHandler 的话,系统会调用这个方法,来告诉你大限已到。你能够选择什么都不作,这样的话系统将看成什么都没发生,简单地显示原来的通知。可能你其实已经设置好了绝大部份内容,只是有不多一部分没有完成,这时你也能够像例子中这样调用contentHandler 来显示一个变动“中途”的通知。

Service Extension 如今只对远程推送的通知起效,你能够在推送 payload 中增长一个 mutable-content 值为 1 的项来启用内容修改:

{
  "aps":{ "alert":{ "title":"Greetings", "body":"Long time no see" }, "mutable-content":1 } } 

这个 payload 的推送获得的结果,注意 body 后面附上了名字。

使用在本机截取推送并替换内容的方式,能够完成端到端 (end-to-end) 的推送加密。你在服务器推送 payload 中加入加密过的文本,在客户端接到通知后使用预先定义或者获取过的密钥进行解密,而后当即显示。这样一来,即便推送信道被第三方截取,其中所传递的内容也仍是安全的。使用这种方式来发送密码或者敏感信息,对于一些金融业务应用和聊天应用来讲,应该是必备的特性。

在通知中展现图片/视频

相比于旧版本的通知,iOS 10 中另外一个亮眼功能是多媒体的推送。开发者如今能够在通知中嵌入图片或者视频,这极大丰富了推送内容的可读性和趣味性。

为本地通知添加多媒体内容十分简单,只须要经过本地磁盘上的文件 URL 建立一个 UNNotificationAttachment 对象,而后将这个对象放到数组中赋值给 content 的 attachments 属性就好了:

let content = UNMutableNotificationContent() content.title = "Image Notification" content.body = "Show me an image!" if let imageURL = Bundle.main.url(forResource: "image", withExtension: "jpg"), let attachment = try? UNNotificationAttachment(identifier: "imageAttachment", url: imageURL, options: nil) { content.attachments = [attachment] } 

在显示时,横幅或者弹窗将附带设置的图片,使用 3D Touch pop 通知或者下拉通知显示详细内容时,图片也会被放大展现:

除了图片之外,通知还支持音频以及视频。你能够将 MP3 或者 MP4 这样的文件提供给系统来在通知中进行展现和播放。不过,这些文件都有尺寸的限制,好比图片不能超过 5MB,视频不能超过 50MB 等,不过对于通常的能在通知中展现的内容来讲,这个尺寸应该是绰绰有余了。关于支持的文件格式和尺寸,能够在文档中进行确认。在建立 UNNotificationAttachment 时,若是遇到了不支持的格式,SDK 也会抛出错误。

经过远程推送的方式,你也能够显示图片等多媒体内容。这要借助于上一节所提到的经过 Notification Service Extension 来修改推送通知内容的技术。通常作法是,咱们在推送的 payload 中指定须要加载的图片资源地址,这个地址能够是应用 bundle 内已经存在的资源,也能够是网络的资源。不过由于在建立 UNNotificationAttachment 时咱们只能使用本地资源,因此若是多媒体还不在本地的话,咱们须要先将其下载到本地。在完成UNNotificationAttachment 建立后,咱们就能够和本地通知同样,将它设置给 attachments 属性,而后调用 contentHandler 了。

简单的示例 payload 以下:

{
  "aps":{ "alert":{ "title":"Image Notification", "body":"Show me an image from web!" }, "mutable-content":1 }, "image": "https://onevcat.com/assets/images/background-cover.jpg" } 

mutable-content 表示咱们会在接收到通知时对内容进行更改,image 指明了目标图片的地址。

在 NotificationService 里,加入以下代码来下载图片,并将其保存到磁盘缓存中:

private func downloadAndSave(url: URL, handler: @escaping (_ localURL: URL?) -> Void) { let task = URLSession.shared.dataTask(with: url, completionHandler: { data, res, error in var localURL: URL? = nil if let data = data { let ext = (url.absoluteString as NSString).pathExtension let cacheURL = URL(fileURLWithPath: FileManager.default.cachesDirectory) let url = cacheURL.appendingPathComponent(url.absoluteString.md5).appendingPathExtension(ext) if let _ = try? data.write(to: url) { localURL = url } } handler(localURL) }) task.resume() } 

而后在 didReceive: 中,接收到这类通知时提取图片地址,下载,并生成 attachment,进行通知展现:

if let imageURLString = bestAttemptContent.userInfo["image"] as? String, let URL = URL(string: imageURLString) { downloadAndSave(url: URL) { localURL in if let localURL = localURL { do { let attachment = try UNNotificationAttachment(identifier: "image_downloaded", url: localURL, options: nil) bestAttemptContent.attachments = [attachment] } catch { print(error) } } contentHandler(bestAttemptContent) } } 

关于在通知中展现图片或者视频,有几点想补充说明:

  • UNNotificationContent 的 attachments 虽然是一个数组,可是系统只会展现第一个 attachment 对象的内容。不过你依然能够发送多个 attachments,而后在要展现的时候再从新安排它们的顺序,以显示最符合情景的图片或者视频。另外,你也可能会在自定义通知展现 UI 时用到多个 attachment。咱们接下来一节中会看到一个相关的例子。
  • 在当前 beta (iOS 10 beta 4) 中,serviceExtensionTimeWillExpire被调用以前,你有 30 秒时间来处理和更改通知内容。对于通常的图片来讲,这个时间是足够的。可是若是你推送的是体积较大的视频内容,用户又恰巧处在糟糕的网络环境的话,颇有可能没法及时下载完成。
  • 若是你想在远程推送来的通知中显示应用 bundle 内的资源的话,要注意 extension 的 bundle 和 app main bundle 并非一回事儿。你能够选择将图片资源放到 extension bundle 中,也能够选择放在 main bundle 里。总之,你须要保证可以获取到正确的,而且你具备读取权限的 url。关于从 extension 中访问 main bundle,能够参看这篇回答
  • 系统在建立 attachement 时会根据提供的 url 后缀肯定文件类型,若是没有后缀,或者后缀没法不正确的话,你能够在建立时经过UNNotificationAttachmentOptionsTypeHintKey 来指定资源类型
  • 若是使用的图片和视频文件不在你的 bundle 内部,它们将被移动到系统的负责通知的文件夹下,而后在当通知被移除后删除。若是媒体文件在 bundle 内部,它们将被复制到通知文件夹下。每一个应用能使用的媒体文件的文件大小总和是有限制,超过限制后建立 attachment 时将抛出异常。可能的全部错误能够在 UNError 中找到。
  • 你能够访问一个已经建立的 attachment 的内容,可是要注意权限问题。可使用 startAccessingSecurityScopedResource 来暂时获取以建立的 attachment 的访问权限。好比:

    let content = notification.request.content if let attachment = content.attachments.first { if attachment.url.startAccessingSecurityScopedResource() { eventImage.image = UIImage(contentsOfFile: attachment.url.path!) attachment.url.stopAccessingSecurityScopedResource() } } 

关于 Service Extension 和多媒体通知的使用,能够参考 Demo 中NotificationService 和 MediaViewController 的内容。

自定义通知视图样式

iOS 10 SDK 新加的另外一个 Content Extension 能够用来自定义通知的详细页面的视图。新建一个 Notification Content Extension,Xcode 为咱们准备的模板中包含了一个实现了 UNNotificationContentExtension 的UIViewController 子类。这个 extension 中有一个必须实现的方法didReceive(_:),在系统须要显示自定义样式的通知详情视图时,这个方法将被调用,你须要在其中配置你的 UI。而 UI 自己能够经过这个 extension 中的 MainInterface.storyboard 来进行定义。自定义 UI 的通知是和通知 category 绑定的,咱们须要在 extension 的 Info.plist 里指定这个通知样式所对应的 category 标识符:

系统在接收到通知后会先查找有没有可以处理这类通知的 content extension,若是存在,那么就交给 extension 来进行处理。另外,在构建 UI 时,咱们能够经过 Info.plist 控制通知详细视图的尺寸,以及是否显示原始的通知。关于 Content Extension 中的 Info.plist 的 key,能够在这个文档中找到详细信息。

虽然咱们可使用包括按钮在内的各类 UI,可是系统不容许咱们对这些 UI 进行交互。点击通知视图 UI 自己会将咱们导航到应用中,不过咱们能够经过 action 的方式来对自定义 UI 进行更新。UNNotificationContentExtension为咱们提供了一个可选方法 didReceive(_:completionHandler:),它会在用户选择了某个 action 时被调用,你有机会在这里更新通知的 UI。若是有 UI 更新,那么在方法的 completionHandler 中,开发者能够选择传递.doNotDismiss 来保持通知继续被显示。若是没有继续显示的必要,能够选择 .dismissAndForwardAction 或者 .dismiss,前者将把通知的 action 继续传递给应用的 UNUserNotificationCenterDelegate 中的userNotificationCenter(:didReceive:withCompletionHandler),然后者将直接解散这个通知。

若是你的自定义 UI 包含视频等,你还能够实现UNNotificationContentExtension 里的 media 开头的一系列属性,它将为你提供一些视频播放的控件和相关方法。

关于 Content Extension 和自定义通知样式,能够参考 Demo 中NotificationViewController 和 CustomizeUIViewController 的内容。

总结

iOS 10 SDK 中对通知这块进行了 iOS 系统发布以来最大的一次重构,不少“老朋友”都被标记为了 deprecated:

iOS 10 中被标为弃用的 API

  • UILocalNotification
  • UIMutableUserNotificationAction
  • UIMutableUserNotificationCategory
  • UIUserNotificationAction
  • UIUserNotificationCategory
  • UIUserNotificationSettings
  • handleActionWithIdentifier:forLocalNotification:
  • handleActionWithIdentifier:forRemoteNotification:
  • didReceiveLocalNotification:withCompletion:
  • didReceiveRemoteNotification:withCompletion:

等一系列在 UIKit 中的发送和处理通知的类型及方法。

现状以及尽快使用新的 API

相比于 iOS 早期时代的 API,新的 API 展示出了高度的模块化和统一特性,易用性也很是好,是一套更加先进的 API。若是有可能,特别是若是你的应用是重度依赖通知特性的话,直接从 iOS 10 开始可让你充分使用在新通知体系的各类特性。

虽然原来的 API 都被标为弃用了,可是若是你须要支持 iOS 10 以前的系统的话,你仍是须要使用原来的 API。咱们可使用

if #available(iOS 10.0, *) { // Use UserNotification } 

的方式来指针对 iOS 10 进行新通知的适配,并让 iOS 10 的用户享受到新通知带来的便利特性,而后在未来版本升级到只支持 iOS 10 以上时再移除掉全部被弃用的代码。对于优化和梳理通知相关代码来讲,新 API 对代码设计和组织上带来的好处足以弥补适配上的麻烦,并且它还能为你的应用提供更好的通知特性和体验,何乐不为呢?

 

转载至原文地址:https://onevcat.com/2016/08/notification/

相关文章
相关标签/搜索