iOS 10使用独立的 UserNotifications.framework 来集中管理和使用 iOS 系统中通知的功能。在此基础上,Apple 还增加了撤回单条通知,更新已展示通知,中途修改通知内容,在通知中展示图片视频,自定义通知 UI 等一系列新功能,非常强大。
WWDC 视频:
https://developer.apple.com/videos/play/wwdc2016/707/
https://developer.apple.com/videos/play/wwdc2016/708/
官方文档:
https://developer.apple.com/reference/usernotifications
历史:
从在 iOS 3 引入 Push Notification 后,之后几乎每个版本 Apple 都在加强这方面的功能。我们可以回顾一下整个历程和相关的主要 API:
iOS 3 - 引入推送通知 UIApplication 的 registerForRemoteNotificationTypes 与UIApplicationDelegate 的application(_:didRegisterForRemoteNotificationsWithDeviceToken:),application(_:didReceiveRemoteNotification:)
iOS 4 - 引入本地通知scheduleLocalNotification,presentLocalNotificationNow:,application(_:didReceive:)
iOS 5 - 加入通知中心页面
iOS 6 - 通知中心页面与 iCloud 同步
iOS 7 - 后台静默推送application(_:didReceiveRemoteNotification:fetchCompletionHandle:)
iOS 8 - 重新设计 notification 权限请求,Actionable 通知registerUserNotificationSettings(_:),UIUserNotificationAction 与UIUserNotificationCategory,application(_:handleActionWithIdentifier:forRemoteNotification:completionHandler:) 等。
在iOS 8 中,我们可以给推送增加用户操作,这样使推送更加具有交互性,并且允许用户去处理用户推送更加的迅速。
iOS 9 - Text Input action,基于 HTTP/2 的推送请求UIUserNotificationActionBehavior,全新的 Provider API 等
到了iOS 9 中,苹果又再次增加了快速回复功能,进一步的提高了通知的响应性。开发者可以允许用户通过点击推送,并用文字进行回复。
通知功能相对还是简单,我们能做的只是本地或者远程发起通知,然后显示给用户。虽然 iOS 8 和 9 中添加了按钮和文本来进行交互,但是已发出的通知不能更新,通知的内容也只是在发起时唯一确定,而这些内容也只能是简单的文本。 想要在现有基础上扩展通知的功能,势必会让原本就盘根错节的 API 更加难以理解。
权限申请,注册远程通知
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge, .carPlay]) {//申请权限
granted, error in
if granted {
UIApplication.shared.registerForRemoteNotifications()//用户同意后可注册远程通知 ,来获得token
} else {
if let error = error {
UIAlertController.showConfirmAlert(message: error.localizedDescription, in: self)
}
}
}
注册token后的回调(在新notification架构中没有变的两个接口)
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenString = deviceToken.hexString
UserDefaults.standard.set(tokenString, forKey: "push-token")
NotificationCenter.default.post(name: .AppDidReceivedRemoteNotificationDeviceToken, object: nil, userInfo: [Notification.Key.AppDidReceivedRemoteNotificationDeviceTokenKey: tokenString])
print("Get Push token: \(tokenString)")
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
UserDefaults.standard.set("", forKey: "push-token")
}
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...
}
发送通知:
本地:
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)")
} else {
print("Notification request added: \(identifier)")
}
}
2、触发器可以区分本地和远程通知,本地通知提供的三个触发器:
在一定时间后触发UNTimeIntervalNotificationTrigger,
在某月某日某时触发UNCalendarNotificationTrigger
在用户进入或是离开某个区域时触发UNLocationNotificationTrigger。
远程推送的通知的话默认会在收到后立即显示,远程通知触发器:UNPushNotificationTrigger
3、请求标识符可以用来区分不同的通知请求,在将一个通知请求提交后,通过特定 API 我们能够使用这个标识符来取消或者更新这个通知
4、在新版本的通知框架中,Apple 借用了一部分网络请求的概念。我们组织并发送一个通知请求,然后将这个请求提交给 UNUserNotificationCenter 进行处理。我们会在 delegate 中接收到这个通知请求对应的 response,另外我们也有机会在应用的 extension 中对 request 进行处理。
取消和更新
在创建通知请求时,我们已经指定了标识符。这个标识符可以用来管理通知。在 iOS 10 之前,我们很难取消掉某一个特定的通知,也不能主动移除或者更新已经展示的通知。想象一下你需要推送用户账户内的余额变化情况,多次的余额增减或者变化很容易让用户十分困惑 - 到底哪条通知才是最正确的?又或者在推送一场比赛的比分时,频繁的通知必然导致用户通知中心数量爆炸,而大部分中途的比分对于用户来说只是噪音。
iOS 10 中,UserNotifications 框架提供了一系列管理通知的 API,你可以做到:
更新通知:
本地通知
* 更新还未展示或已经展示过的通知
和一开始添加请求时一样,再次将请求提交给UNUserNotificationCenter
远程通知
在使用 Provider API 向 APNs 提交请求时,在 HTTP/2 的 header 中apns-collapse-id key 的内容将被作为该推送的标识符进行使用。多次推送同一标识符的通知即可进行更新。
取消通知:
本地通知
* 取消还未展示的通知
open func removePendingNotificationRequests(withIdentifiers identifiers: [String])
open func removeAllPendingNotificationRequests()
* 取消已经展示过的通知
open func removeDeliveredNotifications(withIdentifiers identifiers: [String])
open func removeAllDeliveredNotifications()
远程通知
对应本地的 removeDeliveredNotifications,现在还不能通过类似的方式,向 APNs 发送一个包含 collapse id 的 DELETE 请求来删除已经展示的推送,APNs 服务器并不接受一个 DELETE 请求。不过从技术上来说 Apple 方面应该不存在什么问题,我们可以拭目以待。现在如果想要消除一个远程推送,可以选择使用后台静默推送的方式来从本地发起一个删除通知的调用。
处理通知
UNUserNotificationCenterDelegate提供了两个方法:
@available(iOS 10.0, *)
public protocol UNUserNotificationCenterDelegate : NSObjectProtocol {
//如何在应用内展示通知
// The method will be called on the delegate only if the application is in the foreground. If the method is not implemented or the handler is not called in a timely manner then the notification will not be presented. The application can choose to have the notification presented as a sound, badge, alert and/or in the notification list. This decision should be based on whether the information in the notification is otherwise visible to the user.
@available(iOS 10.0, *)
optional public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Swift.Void)
//收到通知响应时要如何处理的工作
// The method will be called on the delegate when the user responded to the notification by opening the application, dismissing the notification or choosing a UNNotificationAction. The delegate must be set before the application returns from applicationDidFinishLaunching:.
@available(iOS 10.0, *)
optional public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Swift.Void)
}
- 应用内展示通知:
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
// 可以根据notification.request.identifier的值做出不同业务的反应:
completionHandler([.alert, .sound])
// 如果不想显示某个通知,可以直接用空 options 调用 completionHandler:
// completionHandler([])
}
- 对通知进行响应
userNotificationCenter(_:didReceive:withCompletionHandler:)。这个代理方法会在用户与你推送的通知进行交互时被调用,包括用户通过通知打开了你的应用,或者点击或者触发了某个 action。因为涉及到打开应用的行为,所以实现了这个方法的 delegate 必须在applicationDidFinishLaunching: 返回前就完成设置:
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
/*
进行自己的业务操作,UNNotificationResponse 是一个几乎包括了通知的所有信息的对象
*/
completionHandler()
}
- Actionable 通知发送和处理
1、注册 Category:
iOS 8 和 9 中 Apple 引入了可以交互的通知,这是通过将一簇 action 放到一个 category 中,将这个 category 进行注册,最后在发送通知时将通知的 category 设置为要使用的 category 来实现的。
private func registerNotificationCategory() {
let saySomethingCategory: UNNotificationCategory = {
let inputAction = UNTextInputNotificationAction(
identifier: SaySomethingCategoryAction.input.rawValue,
title: "Input",
options: [.foreground],
textInputButtonTitle: "Send",
textInputPlaceholder: "What do you want to say...")
let goodbyeAction = UNNotificationAction(
identifier: SaySomethingCategoryAction.goodbye.rawValue,
title: "Goodbye",
options: [.foreground])
let cancelAction = UNNotificationAction(
identifier: SaySomethingCategoryAction.none.rawValue,
title: "Cancel",
options: [.destructive])
return UNNotificationCategory(identifier: UserNotificationCategoryType.saySomething.rawValue, actions: [inputAction, goodbyeAction, cancelAction], intentIdentifiers: [], options: [.customDismissAction])
}()
let customUICategory: UNNotificationCategory = {
let nextAction = UNNotificationAction(
identifier: CustomizeUICategoryAction.switch.rawValue,
title: "Switch",
options: [])
let openAction = UNNotificationAction(
identifier: CustomizeUICategoryAction.open.rawValue,
title: "Open",
options: [.foreground])
let dismissAction = UNNotificationAction(
identifier: CustomizeUICategoryAction.dismiss.rawValue,
title: "Dismiss",
options: [.destructive])
return UNNotificationCategory(identifier: UserNotificationCategoryType.customUI.rawValue, actions: [nextAction, openAction, dismissAction], intentIdentifiers: [], options: [])
}()
UNUserNotificationCenter.current().setNotificationCategories([saySomethingCategory, customUICategory])
}
1. UNTextInputNotificationAction 代表一个输入文本的 action,你可以自定义框的按钮 title 和 placeholder。你稍后会使用 identifier 来对 action 进行区分。
2. 普通的 UNNotificationAction 对应标准的按钮。
3. 为 category 指定一个 identifier,我们将在实际发送通知的时候用这个标识符进行设置,这样系统就知道这个通知对应哪个 category 了。
本地发送带action通知:
在完成 category 注册后,发送一个 actionable 通知就非常简单了,只需要在创建UNNotificationContent 时把 categoryIdentifier 设置为需要的 category id 即可:
content.categoryIdentifier = "saySomethingCategory"
2、远程发送带action通知:
只需要在 payload 中添加 category 字段,并指定预先定义的 category id 就可以了:
{
"aps":{
"alert":"Please say something",
"category":"saySomething"
}
}
3、处理action通知:
和普通的通知并无二致,actionable 通知也会走到 didReceive 的 delegate 方法,我们通过 request 中包含的 categoryIdentifier 和 response 里的 actionIdentifier 就可以轻易判定是哪个通知的哪个操作被执行了。对于 UNTextInputNotificationAction 触发的 response,直接将它转换为一个 UNTextInputNotificationResponse,就可以拿到其中的用户输入了:
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
// response.notification.request.content.categoryIdentifier
completionHandler()
}
在通知中展示图片/视频
相比于旧版本的通知,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 这样的文件提供给系统来在通知中进行展示和播放。不过,这些文件都有尺寸的限制。关于支持的文件格式和尺寸,可以在https://developer.apple.com/reference/usernotifications/unnotificationattachment中进行确认(系统会自动提供一套可自定义化的UI,专门针对这3种内容。)。在创建 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(Content Extension)。
* 在当前 beta (iOS 10 beta 4) 中,serviceExtensionTimeWillExpire 被调用之前,你有 30 秒时间来处理和更改通知内容。对于一般的图片来说,这个时间是足够的。但是如果你推送的是体积较大的视频内容,用户又恰巧处在糟糕的网络环境的话,很有可能无法及时下载完成。
* 如果你想在远程推送来的通知中显示应用 bundle 内的资源的话,要注意 extension 的 bundle 和 app main bundle 并不是一回事儿。你可以选择将图片资源放到 extension bundle 中,也可以选择放在 main bundle 里。总之,你需要保证能够获取到正确的,并且你具有读取权限的 url。关于从 extension 中访问 main bundle,可以参看http://stackoverflow.com/questions/26189060/get-the-main-app-bundle-from-within-extension。
The +mainBundle method returns the bundle containing the "current application executable", which is a subfolder of your app when called from within an extension.
The solution that I've found involved peeling off two directory levels from the URL of the bundle, when it ends in "appex".
Objective-C NSBundle *bundle = [NSBundle mainBundle];
if ([[bundle.bundleURL pathExtension] isEqualToString:@"appex"]) {
// Peel off two directory levels - MY_APP.app/PlugIns/MY_APP_EXTENSION.appex
bundle = [NSBundle bundleWithURL:[[bundle.bundleURL URLByDeletingLastPathComponent] URLByDeletingLastPathComponent]];
}
NSString *appDisplayName = [bundle objectForInfoDictionaryKey:@"CFBundleDisplayName"]; Swift 2.2 var bundle = NSBundle.mainBundle()
if bundle.bundleURL.pathExtension == "appex" {
// Peel off two directory levels - MY_APP.app/PlugIns/MY_APP_EXTENSION.appex
bundle = NSBundle(URL: bundle.bundleURL.URLByDeletingLastPathComponent!.URLByDeletingLastPathComponent!)!
}
let appDisplayName = bundle.objectForInfoDictionaryKey("CFBundleDisplayName") Swift 3 var bundle = Bundle.main()
if bundle.bundleURL.pathExtension == "appex" {
// Peel off two directory levels - MY_APP.app/PlugIns/MY_APP_EXTENSION.appex
do {
try bundle = Bundle(url: bundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent())!
} catch {
print("Could not delete lastPathComponent")
}
}
let appDisplayName = bundle.objectForInfoDictionaryKey("CFBundleDisplayName") This will break if the pathExtension or the directory structure for an iOS extension ever changes.
* 系统在创建 attachement 时会根据提供的 url 后缀确定文件类型,如果没有后缀,或者后缀无法不正确的话,你可以在创建时通过 UNNotificationAttachmentOptionsTypeHintKey 来指定资源类型(https://developer.apple.com/reference/usernotifications/unnotificationattachmentoptionstypehintkey)。
* 如果使用的图片和视频文件不在你的 bundle 内部,它们将被移动到系统的负责通知的文件夹下,然后在当通知被移除后删除。如果媒体文件在 bundle 内部,它们将被复制到通知文件夹下。每个应用能使用的媒体文件的文件大小总和是有限制,超过限制后创建 attachment 时将抛出异常。可能的所有错误可以在 UNError 中找到。
* 你可以访问一个已经创建的 attachment 的内容,但是要注意权限问题。attachment是由系统管理的,系统会把它们单独的管理,这意味着它们存储在我们sandbox之外。所以这里我们要使用attachment之前,我们需要告诉iOS系统,我们需要使用它,并且在使用完毕之后告诉系统我们使用完毕了。对应上述代码就是startAccessingSecurityScopedResource()和stopAccessingSecurityScopedResource()的操作。比如
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()
}
}
Notification Extension
iOS 10 中添加了很多 extension,作为应用与系统整合的入口。与通知相关的 extension 有两个:
Service Extension :可以让我们有机会在收到远程推送的通知后,展示之前对通知内容进行修改
Content Extension:可以用来自定义通知视图的样式。
Service Extension :
例如上面远程收到图片url:
import UserNotifications
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
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()
}
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)
} else if request.identifier == "media" {
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)
}
}
}
}
}
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)
}
}
}
extension FileManager {
var cachesDirectory: String {
var paths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true) as [String]
return paths[0]
}
}
extension String {
var md5: String {
var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
if let data = data(using: .utf8) {
data.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
CC_MD5(bytes, CC_LONG(data.count), &digest)
}
}
var digestHex = ""
for index in 0..<Int(CC_MD5_DIGEST_LENGTH) {
digestHex += String(format: "%02x", digest[index])
}
return digestHex
}
}
1. didReceive: 方法中有一个等待发送的通知请求,我们通过修改这个请求中的 content 内容,然后在限制的时间内将修改后的内容调用通过 contentHandler 返还给系统,就可以显示这个修改过的通知了。
2. 在一定时间内没有调用 contentHandler 的话,系统会调用这个方法,来告诉你大限已到。你可以选择什么都不做,这样的话系统将当作什么都没发生,简单地显示原来的通知。可能你其实已经设置好了绝大部分内容,只是有很少一部分没有完成,这时你也可以像例子中这样调用contentHandler 来显示一个变更“中途”的通知。
Service Extension 现在只对远程推送的通知起效,你可以在推送 payload 中增加一个 mutable-content 值为 1 的项来启用内容修改:
用途:
使用在本机截取推送并替换内容的方式,可以完成端到端 (end-to-end) 的推送加密。你在服务器推送 payload 中加入加密过的文本,在客户端接到通知后使用预先定义或者获取过的密钥进行解密,然后立即显示。这样一来,即使推送信道被第三方截取,其中所传递的内容也还是安全的。使用这种方式来发送密码或者敏感信息,对于一些金融业务应用和聊天应用来说,应该是必备的特性。
推送通知中带了push payload,及时去年苹果已经把payload的size提升到了4k bites,但是这么小的容量也无法使用户能发送一张高清的图片,甚至把这张图的缩略图包含在推送通知里面,也不一定放的下去。在iOS X中,我们可以使用新特性来解决这个问题。我们可以通过新的service extensions来解决这个问题。
Content Extension:
可以用来自定义通知的详细页面的视图。新建一个 Notification Content Extension,Xcode 为我们准备的模板中包含了一个实现了UNNotificationContentExtension 的 UIViewController 子类。
这个 extension 中有一个必须实现的方法 didReceive(_:),在系统需要显示自定义样式的通知详情视图时,这个方法将被调用,你需要在其中配置你的 UI。而 UI 本身可以通过这个 extension 中的 MainInterface.storyboard 来进行定义。自定义 UI 的通知是和通知 category 绑定的,我们需要在 extension 的 Info.plist 里指定这个通知样式所对应的 category 标识符(值得提到的一点是,这里的extension是可以为一个数组的,里面可以为多个category,这样做的目的是多个category共用同一套UI。)我们自定义的内容和default conetnt的推送内容重复了,可以设置UNNotificationExtensionDefaultContentHidden为YES去掉
系统在接收到通知后会先查找有没有能够处理这类通知的 content extension,如果存在,那么就交给 extension 来进行处理。另外,在构建 UI 时,我们可以通过 Info.plist 控制通知详细视图的尺寸,以及是否显示原始的通知。关于 Content Extension 中的 Info.plist 的 key,可以在https://developer.apple.com/reference/usernotificationsui/unnotificationcontentextension中找到详细信息。
虽然我们可以使用包括按钮在内的各种 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 界面:

上图,整个推送分4段。用户可以通过点击Header里面的icon来打开app,点击取消来取消显示推送。
Header的UI是系统提供的一套标准的UI。这套UI会提供给所有的推送通知。
Header下面是自定义内容,这里就是显示的Notification content extension。在这里,就可以显示任何你想绘制的内容了。你可以展示任何额外的有用的信息给用户。
content extension下面就是default content。这里是系统的界面。这里的系统界面就是上面推送里面payload里面附带的内容。这也就是iOS 9 之前的推送的样子。
最下面一段就是notification action了。在这一段,用户可以触发一些操作。并且这些操作还会相应的反映到上面的自定义的推送界面content extension中。
在xcode8中兼容iOS10和老系统:
1、
targets->capabilities->push notifications 必须打开,否则iOS会注册失败的
2、
虽然原来的 API 都被标为弃用了,但是如果你需要支持 iOS 10 之前的系统的话,你还是需要使用原来的 API。我们可以使用适配:
if #available(iOS 10.0, *) {
// Use UserNotification
}
感谢:
活久见的重构 - iOS 10 UserNotifications 框架解析 https://onevcat.com/2016/08/notification/
iOS8 notification http://justsee.iteye.com/blog/2144285
WWDC2016 Session笔记 - iOS 10 推送Notification新特性 http://www.jianshu.com/p/9b720efe3779
玩转 iOS 10 推送 —— UserNotifications Framework(上) http://www.jianshu.com/p/2f3202b5e758
玩转 iOS 10 推送 —— UserNotifications Framework(中) http://www.jianshu.com/p/5a4b88874f3a
iOS 推送全解析,你不可不知的所有 Tips!http://www.jianshu.com/p/e9c313df746f
iOS10 推送必看(基础篇) http://ios.jobbole.com/89277/
iOS10推送必看(高阶1)http://ios.jobbole.com/89283/
iOS推送——本地推送与远程推送详解(一图看懂)http://ios.jobbole.com/85200/
实现 iOS 前台时的推送弹窗效果 http://www.jianshu.com/p/67864e1c2085