iOS,object-c和swift开发直播 IMiOS开发 技术集锦

喜闻乐见的重构——iOS 10 UserNotification

2016-09-05  本文已影响732人  老狼灰灰
ios10

随着iOS10的发布越来越近,开发者适配iOS10系统的脚步也越来越快。在以往iOS系统中,有一个功能一直被开发者所诟病——推送通知。

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

从iOS3.0开始,推送通知出现在所有人的眼前。一直到iOS9,通知的功能越来越强大,随之而来的是api越来越混乱。我们可以回顾一下整个历程和相关的主要 API:

iOS 3 – 引入推送通知 UIApplication 的  与 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 9 – Text Input action,基于 HTTP/2 的推送请求UIUserNotificationActionBehavior,全新的 Provider API 等

一个开发者很难在不借助于文档的帮助下区分

application(_:didReceiveRemoteNotification:)

application(_:didReceiveRemoteNotification:fetchCompletionHandle:)

新入行的开发者也不可能明白

registerForRemoteNotificationTypes

registerUserNotificationSettings(_:)

之间是不是有什么关系,Remote 和 Local Notification 除了在初始化方式之外那些细微的区别也让人抓狂,而很多 API 都被随意地放在了 UIApplication 或者 UIApplicationDelegate 中。

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

发自简书APP

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

通知流程

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

那么,怎么在应用中使用推送呢?

在Xcode中启用推送通知是很容易的,但你需要几个步骤。

创建一个新的工程,给它起一个唯一的Bundle Identifier.

当您已经创建了project,去Project Settings页选择Capabilities栏。打开推送通知,如下所示。

注意: 如果你是苹果的付费开发者成员,你就能看到推送通知功能这一栏。

发自简书APP

去Developer Account这一栏,从左侧的菜单栏中选择证书,IDs,和描述文件,然后选择App IDs在Identifiers栏中。找到已经创建的App的名称,在服务列表中选中。注意,有两个可配置状态的推送通知。

发自简书APP

要想发送通知,还需要一个证书,这个证书可以在mac的“钥匙串访问”中得到。

打开Keychain Access,从证书认证菜单中选择Keychain Access -> Certificate Assistant -> Request a Certificate。

发自简书APP 发自简书APP

填写表单并单击Continue。确保你选择保存到了硬盘。

发自简书APP

返回到开发者账户的网页。你可以为你的App IDs生成开发(调试)证书或发布证书。之后在选择右侧的申请,在底部,单击编辑。在推送通知部分,单击创建开发(调试)证书。

发自简书APP

在需要时,从Keychain,继续上传生成证书请求。

发自简书APP

现在你已经创建了证书,可以下载它。打开下载的文件安装它。

发自简书APP

权限申请

通用权限

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) -> 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)")

}

}

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,可以参考这篇文档

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

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

在新版本的通知框架中,Apple 借用了一部分网络请求的概念。我们组织并发送一个通知请求,然后将这个请求提交给 UNUserNotificationCenter 进行处理。我们会在 delegaet 中接收到这个通知请求对应的 response,另外我们也有机会在应用的 extension 中对 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 来实现的。

发自简书APP

注册一个 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])

}

UNTextInputNotificationAction 代表一个输入文本的 action,你可以自定义框的按钮 title 和 placeholder。你稍后会使用 identifier 来对 action 进行区分。

普通的 UNNotificationAction 对应标准的按钮。

为 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"

远程推送也可以使用 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 等。

Notification Extension

发自简书APP

上图,整个推送分4段。用户可以通过点击Header里面的icon来打开app,点击取消来取消显示推送。Header的UI是系统提供的一套标准的UI。这套UI会提供给所有的推送通知。

Header下面是自定义内容,这里就是显示的Notification content extension。在这里,就可以显示任何你想绘制的内容了。你可以展示任何额外的有用的信息给用户。

content extension下面就是default content。这里是系统的界面。这里的系统界面就是上面推送里面payload里面附带的内容。这也就是iOS 9 之前的推送的样子。

最下面一段就是notification action了。在这一段,用户可以触发一些操作。并且这些操作还会相应的反映到上面的自定义的推送界面content extension中。

发自简书APP

截取并修改通知内容

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)

}

}

}

didReceive: 方法中有一个等待发送的通知请求,我们通过修改这个请求中的 content 内容,然后在限制的时间内将修改后的内容调用通过 contentHandler 返还给系统,就可以显示这个修改过的通知了。

在一定时间内没有调用 contentHandler 的话,系统会调用这个方法,来告诉你大限已到。你可以选择什么都不做,这样的话系统将当作什么都没发生,简单地显示原来的通知。可能你其实已经设置好了绝大部分内容,只是有很少一部分没有完成,这时你也可以像例子中这样调用 contentHandler 来显示一个变更“中途”的通知。

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

{

  "aps":{

  "alert":{

     "title":"Greetings",

     "body":"Long time no see"

   },

   "mutable-content":1

   }

}

使用在本机截取推送并替换内容的方式,可以完成端到端 (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。

系统在创建 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()

}

}

自定义通知视图样式

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

发自简书APP

系统在接收到通知后会先查找有没有能够处理这类通知的 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 开头的一系列属性,它将为你提供一些视频播放的控件和相关方法。


关注公众号以获取最新内容
上一篇下一篇

猜你喜欢

热点阅读