一、UserNotifications 框架介绍
1,起源
- 过去我们通过 UILocalNotification 来实现本地消息的推送通知(Local Notification),或者利用 APNS 进行通知消息的远程推送(Remote Notification)。如果我们程序同时用到了本地通知和远程通知,会发现它们的 API 都被随意地放在了 UIApplication 或者 UIApplicationDelegate 中,开发时代码十分混乱。
- 到了 iOS10,苹果新增加了一个 UserNotifications.framework(用户通知框架),目的在于统一 Remote Notification(远程通知)和 Local Notification(本地通知)。过去那些杂乱的和通知相关的 API 都被统一,同时也新增了许多新功能。
2,新特性
UserNotifications 框架除了整合通知相关的 API,还增加了很多令人惊喜的特性,让我们实现许多过去没法实现的功能。
- 更加丰富的推送内容:现在可以设置推送的 title、subtitle、body 以及符合大小的图片、音频、视频等附件内容。
- 更好的通知管理:过去已发出的通知不能更新。现在可以对通知进行查看、更新、删除了(哪怕是已展示通知)。
- 更优雅的展示方式:可以设置应用在前台展示通知,自定义通知 UI。
3,使用流程
UserNotifications 框架的使用大概分为以下几个过程:
- 申请、注册通知:首先需要向用户请求通知权限,在取得权限后注册通知。
- 创建、发送通知:然后创建一个通知并发起推送。对于远程推送 APNS 而言,还需要注册 DeviceToken。
- 展示、处理通知:在接收到推送通知后可以根据 app 的运行情况决定是否展示通知,当然也可以通过一系列的回调接口对通知进行处理加工。
二、通知权限说明
1,申请权限
(1)iOS 10 统一了推送权限的申请。不管是本地推送,还是远程推送,只需要 UNUserNotificationCenter.current().requestAuthorization() 方法申请即可。(这里我们在 AppDelegate 中申请通知权限。当然写在其它地方也是可以的。)
import UIKit import UserNotifications @UIApplicationMain class AppDelegate : UIResponder , UIApplicationDelegate { var window: UIWindow ? func application(_ application: UIApplication , didFinishLaunchingWithOptions launchOptions: [ UIApplicationLaunchOptionsKey : Any ]?) -> Bool { //请求通知权限 UNUserNotificationCenter .current() .requestAuthorization(options: [.alert, .sound, .badge]) { (accepted, error) in if !accepted { print ( "用户不允许消息通知。" ) } } return true } func applicationWillResignActive(_ application: UIApplication ) { } func applicationDidEnterBackground(_ application: UIApplication ) { } func applicationWillEnterForeground(_ application: UIApplication ) { } func applicationDidBecomeActive(_ application: UIApplication ) { } func applicationWillTerminate(_ application: UIApplication ) { } } |
(2)当第一次调用上面这个方法时,系统会弹出如下窗口询问用户是否授权。
(3)如果用户拒绝了这个请求,再次调用该方法也不会再进行弹窗,同时也就无法收到通知。这种情况如果想要应用能接收到通知的话,只能让用户自行前往系统的设置中手动为你的应用打开通知了。因此在合适的时候弹出请求窗,并预先进行说明是很重要的。
2,判断权限
(1)在有些情况下,我们可以对推送权限设置进行检查。比如在检测到用户把通知权限关闭的时候,弹出个提示框引导用户去系统设置中打开通知权限。 比如下面代码,用户如果点击了“设置”按钮,则会自动跳转到通知设置页面,方便用户设置。
UNUserNotificationCenter .current().getNotificationSettings { settings in switch settings.authorizationStatus { case .authorized: return case .notDetermined: //请求授权 UNUserNotificationCenter .current() .requestAuthorization(options: [.alert, .sound, .badge]) { (accepted, error) in if !accepted { print ( "用户不允许消息通知。" ) } } case .denied: DispatchQueue .main.async(execute: { () -> Void in let alertController = UIAlertController (title: "消息推送已关闭" , message: "想要及时获取消息。点击“设置”,开启通知。" , preferredStyle: .alert) let cancelAction = UIAlertAction (title: "取消" , style: .cancel, handler: nil ) let settingsAction = UIAlertAction (title: "设置" , style: . default , handler: { (action) -> Void in let url = URL (string: UIApplicationOpenSettingsURLString ) if let url = url, UIApplication .shared.canOpenURL(url) { if #available(iOS 10, *) { UIApplication .shared.open(url, options: [:], completionHandler: { (success) in }) } else { UIApplication .shared.openURL(url) } } }) alertController.addAction(cancelAction) alertController.addAction(settingsAction) self .present(alertController, animated: true , completion: nil ) }) } } |
(2)除了打开和关闭全部通知权限外,用户也可以限制应用只能进行哪种形式的通知显示,比如:只允许横幅,而不允许声音及通知中心显示等。这些细微的设置,我们程序也是能检测到的。
UNUserNotificationCenter .current().getNotificationSettings { settings in var message = "是否允许通知:" switch settings.authorizationStatus { case .authorized: message.append( "允许" ) case .notDetermined: message.append( "未确定" ) case .denied: message.append( "不允许" ) } message.append( "\n声音:" ) switch settings.soundSetting{ case .enabled: message.append( "开启" ) case .disabled: message.append( "关闭" ) case .notSupported: message.append( "不支持" ) } message.append( "\n应用图标标记:" ) switch settings.badgeSetting{ case .enabled: message.append( "开启" ) case .disabled: message.append( "关闭" ) case .notSupported: message.append( "不支持" ) } message.append( "\n在锁定屏幕上显示:" ) switch settings.lockScreenSetting{ case .enabled: message.append( "开启" ) case .disabled: message.append( "关闭" ) case .notSupported: message.append( "不支持" ) } message.append( "\n在历史记录中显示:" ) switch settings.notificationCenterSetting{ case .enabled: message.append( "开启" ) case .disabled: message.append( "关闭" ) case .notSupported: message.append( "不支持" ) } message.append( "\n横幅显示:" ) switch settings.alertSetting{ case .enabled: message.append( "开启" ) case .disabled: message.append( "关闭" ) case .notSupported: message.append( "不支持" ) } message.append( "\n显示预览:" ) switch settings.showPreviewsSetting{ case .always: message.append( "始终(默认)" ) case .whenAuthenticated: message.append( "解锁时" ) case .never: message.append( "从不" ) } print (message) } |
三、一个简单的本地通知样例
1,效果图
(1)程序启动后会自动创建并发送一个 30 秒后的通知,接着我们便可以锁屏或者将应用切到后台。(2)30 秒时间一到,如果当前是锁屏状态。通知会出现在屏幕横幅中。如果当前是在系统里的话,则会出现在屏幕顶部。当然通知中心里也会有这条通知。
2,样例代码
(1)首先我们在 AppDelegate.swift 中申请通知权限。当然写在其它地方也是可以的,写这里只是为了方便测试,让程序一启动就去申请权限。
import UIKit import UserNotifications @UIApplicationMain class AppDelegate : UIResponder , UIApplicationDelegate { var window: UIWindow ? func application(_ application: UIApplication , didFinishLaunchingWithOptions launchOptions: [ UIApplication . LaunchOptionsKey : Any ]?) -> Bool { //请求通知权限 UNUserNotificationCenter .current() .requestAuthorization(options: [.alert, .sound, .badge]) { (accepted, error) in if !accepted { print ( "用户不允许消息通知。" ) } } return true } func applicationWillResignActive(_ application: UIApplication ) { } func applicationDidEnterBackground(_ application: UIApplication ) { } func applicationWillEnterForeground(_ application: UIApplication ) { } func applicationDidBecomeActive(_ application: UIApplication ) { } func applicationWillTerminate(_ application: UIApplication ) { } } |
(2)然后在程序页面加载完毕后(ViewController.swift)创建一条简单的通知消息(30 秒后触发)。
import UIKit import UserNotifications class ViewController : UIViewController { override func viewDidLoad() { super .viewDidLoad() //设置推送内容 let content = UNMutableNotificationContent () content.title = "hangge.com" content.body = "做最好的开发者知识平台" //设置通知触发器 let trigger = UNTimeIntervalNotificationTrigger (timeInterval: 30, repeats: false ) //设置请求标识符 let requestIdentifier = "com.hangge.testNotification" //设置一个通知请求 let request = UNNotificationRequest (identifier: requestIdentifier, content: content, trigger: trigger) //将通知请求添加到发送中心 UNUserNotificationCenter .current().add(request) { error in if error == nil { print ( "Time Interval Notification scheduled: \(requestIdentifier)" ) } } } override func didReceiveMemoryWarning() { super .didReceiveMemoryWarning() } } |
四、设置推送内容
上面的样例中我们只设置了推送通知的标题(title)和内容(body),其实还可以设置子标题(subtitle)和应用图标标记(badge)。
//设置推送内容 let content = UNMutableNotificationContent () content.title = "hangge.com" content.subtitle = "航歌(二级标题)" content.body = "做最好的开发者知识平台" content.badge = 2 |
效果图如下:
五、设置通知触发器
目前 UserNotifications 框架中一共提供了如下三种触发器。注意:触发器是只对本地通知而言的,远程推送的通知默认会在收到后立即显示。
1,一段时间后触发(UNTimeIntervalNotificationTrigger)
比如下面样例我们设置10秒钟后触发推送通知。
let trigger = UNTimeIntervalNotificationTrigger (timeInterval: 10, repeats: false ) |
2,指定日期时间触发(UNCalendarNotificationTrigger)
(1)下面代码我们设置2017年11月11日凌晨触发推送通知。
var components = DateComponents () components.year = 2017 components.month = 11 components.day = 11 let trigger = UNCalendarNotificationTrigger (dateMatching: components, repeats: false ) |
(2)下面代码我们设置每周一上午8点都会触发推送通知。
var components = DateComponents () components.weekday = 2 //周一 components.hour = 8 //上午8点 components.second = 30 //30分 let trigger = UNCalendarNotificationTrigger (dateMatching: components, repeats: true ) |
3,根据位置触发(UNLocationNotificationTrigger)
该触发器支持进入某地触发、离开某地触发、或者两种情况均触发。下面代码设置成当手机进入到指定点(纬度:52.10,经度:51.11)200 米范围内时会触发推送通知。(注意:这里我们需要 import CoreLocation 框架)
let coordinate = CLLocationCoordinate2D (latitude: 52.10, longitude: 51.11) let region = CLCircularRegion (center: coordinate, radius: 200, identifier: "center" ) region.notifyOnEntry = true //进入此范围触发 region.notifyOnExit = false //离开此范围不触发 let trigger = UNLocationNotificationTrigger (region: region, repeats: true ) |
六、远程推送基本介绍
1,什么是远程推送
- 远程通知是指在联网的情况下,由远程服务器推送给客户端的通知,又称 APNs(Apple Push Notification Services)。
- 由于在联网状态下,所有苹果设备都会与苹果服务器建立长连接。所以不管应用是打开还是关闭的情况,都能接收到服务器推送的远程通知。
2,实现原理
(1)App 打开后首先自动发送 UDID 和 BundleID 给 APNs 注册,并返回 deviceToken。(2)App 获取 deviceToken 后,调用接口将用户身份信息和 deviceToken 发送给我们的服务器,服务器将其记录下来。(3)当要推送消息时,服务器按照用户身份信息找到存储的 deviceToken,将消息和 deviToken 发送给 APNs。(4)苹果的 APNs 通过 deviceToken,找到指定设备的指定程序, 并将消息推送给用户。
3,准备工作
要开发测试远程推送功能,我们需要准备如下两个东西:
- 真机:使用模拟器是没法注册 APNS,自然也就无法实现远程通知。
- 推送证书:这就要求我们必须要有个苹果开发者帐号
4,证书申请
(1)首先我们需要创建应用的 APNs 证书。如果对 APNs 证书不太了解,可以请参考: iOS 证书设置指南。(2)根据指南中的“方式一”,我们创建一个推送证书(aps.cer)。将其下载到本地,并双击安装即可。
七、远程推送样例
1,客户端准备工作
项目配置好证书后,还要打开下图的开关。
2,客户端代码
下面是 AppDelegate.swift 的代码。我们同样是先去获得通知权限后。不过对于 APNs 而言,还需要多一个获取用户 DeviceToken 的操作(高亮部分)。
- 实际应用中我们会把这个 DeviceToken 传递给我的的服务器,服务器后面就可以使用这个 DeviceToken 向 Apple Push Notification 的服务器提交请求,然后 APNs 通过 DeviceToken 识别设备和应用,将通知推给用户。
- 由于获取得到的 DeviceToken 是一个 Data 类型,为了方便使用和传递,通常会将它转换为一个适合传递给 Apple 的字符串(通过 Data 扩展实现)。这里我们直接将转换后的 DeviceToken 字符串打印出来。
import UIKit import UserNotifications @UIApplicationMain class AppDelegate : UIResponder , UIApplicationDelegate { var window: UIWindow ? let notificationHandler = NotificationHandler () func application(_ application: UIApplication , didFinishLaunchingWithOptions launchOptions: [ UIApplicationLaunchOptionsKey : Any ]?) -> Bool { //请求通知权限 UNUserNotificationCenter .current() .requestAuthorization(options: [.alert, .sound, .badge]) { (accepted, error) in if !accepted { print ( "用户不允许消息通知。" ) } } //向APNs请求token UIApplication .shared.registerForRemoteNotifications() return true } //token请求回调 func application(_ application: UIApplication , didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data ) { //打印出获取到的token字符串 print ( "Get Push token: \(deviceToken.hexString)" ) } func applicationWillResignActive(_ application: UIApplication ) { } func applicationDidEnterBackground(_ application: UIApplication ) { } func applicationWillEnterForeground(_ application: UIApplication ) { } func applicationDidBecomeActive(_ application: UIApplication ) { } func applicationWillTerminate(_ application: UIApplication ) { } } //对Data类型进行扩展 extension Data { //将Data转换为String 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 }) } } } |
3,测试运行
(1)接上手机,编译运行程序。可以看到我们已经成功获取到了推送 token。特别要注意的是:
- app 重新启动后,token 是不会变化的。
- app 卸载重新安装的话,token 就会发生变化。
(2)通常来说我们会有个服务端,然后通过这个 token 给对应的设备推送通知。这里为了方便测试,我们使用 APNs 调试工具 Knuff。下载地址:https://github.com/KnuffApp/Knuff/releases
(3)Knuff 使用方法如下:
- Custom:自定义模式。我们测试自己的应用,就用这个模式,可以自行选择证书。
- Choose:选择推送证书。也就是我们文章最开头申请的证书。
- Sandbox:表示推送给开发版本的 App(非 AppStore 版本)。
- Token:即我们上面注册苹果 APNs 服务时获取到的 device token。
- Payload:表示要推送的报文。
(4)上面这些设置好以后,点击“Push”按钮即可发送远程通知。这时手机这边就可以收到这条推送消息。
(5)上面的推送报文比较简单,通知只有一个标题(title)和应用图标标记(badge)。这次我们再增加内容(body)、子标题(subtitle)。
{ “aps”: { “alert”: { “title”:”hangge.com”, “subtitle”:”航歌”, “body”:”做最好的开发者知识平台” }, “sound”: “default”, “badge”: 1 } } |
手机收到通知效果如下:
八、处理通知
UserNotifications 框架为我们提供了查找、更新、删除通知等相关的 API 方法。其中关键在于 request 的 identifier,即在创建时指定的通知标识符。
1,查找通知
(1)获取所有待推送的通知
UNUserNotificationCenter.current().getPendingNotificationRequests { (requests) in //遍历所有未推送的 requestfor request in requests { print(request) } } |
目前我们只有一个未推送的通知:
(2)获取所有已推送的通知
UNUserNotificationCenter.current().getDeliveredNotifications { (notifications) in //遍历所有已推送的通知 for notification in notifications { print(notification) } } |
2,更新通知
多次推送同一标识符的通知即可进行更新。比如我们有一条标识符为“com.hangge.testNotification”的通知还未触发推送。如果创建并添加一条同样标示符的通知,那么原先的那条通知就会被替换。(而如果原先的通知已展示,则会在通知中心中更新这条通知)
//使用同样的请求标识符来设置一个新的通知 let requestIdentifier = “com.hangge.testNotification”let request = UNNotificationRequest(identifier: requestIdentifier,content: content, trigger: trigger) //将通知请求添加到发送中心 UNUserNotificationCenter.current().add(request) { error inif error == nil {print(“Time Interval Notification scheduled: (requestIdentifier)”)}} |
远程推送也可以进行通知的更新:在使用 Provider API 向 APNs 提交请求时,在 HTTP/2 的 header 中 apns-collapse-id key 的内容将被作为该推送的标识符进行使用。多次推送同一标识符的通知即可进行更新。
3,删除通知
(1)取消未发送的通知
//根据identifier来取消指定通知 let identifier = “com.hangge.testNotification”UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [identifier]) //取消全部未发送通知 UNUserNotificationCenter.current().removeAllPendingNotificationRequests() |
(2)删除已发送的通知(清除通知中心里的记录)
//根据identifier来删除指定通知 let identifier = “com.hangge.testNotification”UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [identifier]) //删除全部已发送通知 UNUserNotificationCenter.current().removeAllDeliveredNotifications() |
远程推送无法删除已展示的通知:现在还不能通过类似的方式,向 APNs 发送一个包含 collapse id 的 DELETE 请求来删除已经展示的推送,APNs 服务器并不接受一个 DELETE 请求。
九、应用内展示通知
默认情况下当应用处于前台时,收到的通知是不进行展示的。如果我们希望在应用内也能显示通知的话,需借助 UNUserNotificationCenterDelegate,通过该协议提供的接口方法实现应用内展示通知。
1,样例代码
我们这里在 AppDelegate 中添加相关的代理协议进行处理。
import UIKit import UserNotifications @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? let notificationHandler = NotificationHandler() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { //请求通知权限 UNUserNotificationCenter.current() .requestAuthorization(options: [.alert, .sound, .badge]) { (accepted, error) in if !accepted { print(“用户不允许消息通知。”) } } //设置通知代理 UNUserNotificationCenter.current().delegate = notificationHandler return true } func applicationWillResignActive(_ application: UIApplication) { } func applicationDidEnterBackground(_ application: UIApplication) { } func applicationWillEnterForeground(_ application: UIApplication) { } func applicationDidBecomeActive(_ application: UIApplication) { } func applicationWillTerminate(_ application: UIApplication) { } } class NotificationHandler: NSObject, UNUserNotificationCenterDelegate { //在应用内展示通知 func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert, .sound]) // 如果不想显示某个通知,可以直接用空 options 调用 completionHandler: // completionHandler([]) } } |
2,效果图
可以看到当通知触发时,即使当前应用处于前台,收到的通知也仍然会进行展示。
十、通知的响应回调
UNUserNotificationCenterDelegate 还有另外一个代理方法,会在用户与推送的通知进行交互时被调用。比如:用户通过点击通知打开了应用、点击或触发了某个 action。
1,样例代码
(1)我们在程序页面加载完毕后(ViewController.swift)创建一条简单的通知消息(30 秒后触发)。特别要注意的是创建的通知出了基本的标题内容外,还附上了一些额外的信息。
import UIKit import UserNotifications class ViewController : UIViewController { override func viewDidLoad() { super .viewDidLoad() //设置推送内容 let content = UNMutableNotificationContent () content.title = "hangge.com" content.body = "做最好的开发者知识平台" content.userInfo = [ "userName" : "hangge" , "articleId" : 10086] //设置通知触发器 let trigger = UNTimeIntervalNotificationTrigger (timeInterval: 30, repeats: false ) //设置请求标识符 let requestIdentifier = "com.hangge.testNotification" //设置一个通知请求 let request = UNNotificationRequest (identifier: requestIdentifier, content: content, trigger: trigger) //将通知请求添加到发送中心 UNUserNotificationCenter .current().add(request) { error in if error == nil { print ( "Time Interval Notification scheduled: \(requestIdentifier)" ) } } } override func didReceiveMemoryWarning() { super .didReceiveMemoryWarning() } } |
(2)然后在 AppDelegate 中添加相关的代理协议来处理用户与通知的交互操作。当用户点击通知后,会将该通知的标题、内容以及前面附加的额外信息给打印出来。(实际应用中我们可以根据 userInfo 的内容来决定页面跳转或者是其他后续操作)
import UIKit import UserNotifications @UIApplicationMain class AppDelegate : UIResponder , UIApplicationDelegate { var window: UIWindow ? let notificationHandler = NotificationHandler () func application(_ application: UIApplication , didFinishLaunchingWithOptions launchOptions: [ UIApplicationLaunchOptionsKey : Any ]?) -> Bool { //请求通知权限 UNUserNotificationCenter .current() .requestAuthorization(options: [.alert, .sound, .badge]) { (accepted, error) in if !accepted { print ( "用户不允许消息通知。" ) } } //设置通知代理 UNUserNotificationCenter .current().delegate = notificationHandler return true } func applicationWillResignActive(_ application: UIApplication ) { } func applicationDidEnterBackground(_ application: UIApplication ) { } func applicationWillEnterForeground(_ application: UIApplication ) { } func applicationDidBecomeActive(_ application: UIApplication ) { } func applicationWillTerminate(_ application: UIApplication ) { } } class NotificationHandler : NSObject , UNUserNotificationCenterDelegate { //对通知进行响应(用户与通知进行交互时被调用) func userNotificationCenter(_ center: UNUserNotificationCenter , didReceive response: UNNotificationResponse , withCompletionHandler completionHandler: @escaping () -> Void ) { print (response.notification.request.content.title) print (response.notification.request.content.body) //获取通知附加数据 let userInfo = response.notification.request.content.userInfo print (userInfo) //完成了工作 completionHandler() } } |
2,效果图
(1)程序打开后退出,等待 30 秒后会收到推送通知。
(2)点击通知则自动打开程序,同时控制台中会输出该通知的标题、内容以及附加信息。
十一、Actionable 可交互通知
从 iOS 8 起苹果就引入了可以交互的通知,其实现方式是把一组 action 放到一个 category 中,然后将这个 category 进行注册,最后在发送通知时将通知的 category 设置为要使用的 category 即可。到了 iOS10,苹果又对这些 action 做了统一。
1,效果图
(1)下面是一个带有 action 交互的通知。设备接收到通知后默认的展示效果和普通的通知一样,点击通知同样也会打开应用。
(2)而当我们下拉或者使用 3D touch 展开通知后,就可以看到下方会出现对应的 action 按钮。
(3)这里我们添加了三个按钮,点击后的功能分别如下:
- 点击“点个赞”:自动打开应用,同时在程序界面上弹出用户操作信息。
- 点击“取消”:自动清除通知,且不会打开应用。(如果我们之后手动打开程序,界面上也会弹出用户操作信息。)
- 点击“评论”:界面下方会出现一个输入框。用户输入文字并提交后,会自动打开应用,同时在程序界面上弹出刚才输入的文字内容。
2,样例代码
(1)NotificationHandler.swift为方便管理维护,这里我们将通知响应(action 响应)放在一个单独的文件中。并且将通知的 category 和 action 的标识符都定义成枚举。
import UIKit import UserNotifications //通知category标识符枚举 enum NotificationCategory : String { case news //新闻资讯通知category } //通知category的action标识符枚举 enum NotificationCategoryAction : String { case like case cancel case comment } //通知响应对象 class NotificationHandler : NSObject , UNUserNotificationCenterDelegate { //对通知进行响应(用户与通知进行交互时被调用) func userNotificationCenter(_ center: UNUserNotificationCenter , didReceive response: UNNotificationResponse , withCompletionHandler completionHandler: @escaping () -> Void ) { //根据category标识符做相应的处理 let categoryIdentifier = response.notification.request.content.categoryIdentifier if let category = NotificationCategory (rawValue: categoryIdentifier) { switch category { case .news: handleNews(response: response) } } completionHandler() } //处理新闻资讯通知的交互 private func handleNews(response: UNNotificationResponse ) { let message: String //判断点击是那个action if let actionType = NotificationCategoryAction (rawValue: response.actionIdentifier) { switch actionType { case .like: message = "你点击了“点个赞”按钮" case .cancel: message = "你点击了“取消”按钮" case .comment: message = "你输入的是:\((response as! UNTextInputNotificationResponse).userText)" } } else { //直接点击通知,或者点击删除这个通知会进入这个分支。 message = "" } //弹出相关信息 if !message.isEmpty { showAlert(message: message) } } //在根视图控制器上弹出普通消息提示框 private func showAlert(message: String ) { if let vc = UIApplication .shared.keyWindow?.rootViewController { let alert = UIAlertController (title: nil , message: message, preferredStyle: .alert) alert.addAction( UIAlertAction (title: "确定" , style: .cancel)) vc.present(alert, animated: true ) } } } |
(2)AppDelegate.swift这里除了申请通知权限外,还注册了一个 category,里面包括两个标准按钮 action,以及一个文本输入 action。
import UIKit import UserNotifications @UIApplicationMain class AppDelegate : UIResponder , UIApplicationDelegate { var window: UIWindow ? let notificationHandler = NotificationHandler () func application(_ application: UIApplication , didFinishLaunchingWithOptions launchOptions: [ UIApplicationLaunchOptionsKey : Any ]?) -> Bool { //请求通知权限 UNUserNotificationCenter .current() .requestAuthorization(options: [.alert, .sound, .badge]) { (accepted, error) in if !accepted { print ( "用户不允许消息通知。" ) } } //注册category registerNotificationCategory() UNUserNotificationCenter .current().delegate = notificationHandler return true } func applicationWillResignActive(_ application: UIApplication ) { } func applicationDidEnterBackground(_ application: UIApplication ) { } func applicationWillEnterForeground(_ application: UIApplication ) { } func applicationDidBecomeActive(_ application: UIApplication ) { } func applicationWillTerminate(_ application: UIApplication ) { } //注册一个category private func registerNotificationCategory() { let newsCategory: UNNotificationCategory = { //创建输入文本的action let inputAction = UNTextInputNotificationAction ( identifier: NotificationCategoryAction .comment.rawValue, title: "评论" , options: [.foreground], textInputButtonTitle: "发送" , textInputPlaceholder: "在这里留下你想说的话..." ) //创建普通的按钮action let likeAction = UNNotificationAction ( identifier: NotificationCategoryAction .like.rawValue, title: "点个赞" , options: [.foreground]) //创建普通的按钮action let cancelAction = UNNotificationAction ( identifier: NotificationCategoryAction .cancel.rawValue, title: "取消" , options: [.destructive]) //创建category return UNNotificationCategory (identifier: NotificationCategory .news.rawValue, actions: [inputAction, likeAction, cancelAction], intentIdentifiers: [], options: [.customDismissAction]) }() //把category添加到通知中心 UNUserNotificationCenter .current().setNotificationCategories([newsCategory]) } } |
(3)ViewController.swift 我们在程序界面打开后就创建通知(5 秒后推送)。特别注意的是要将通知的 categoryIdentifier 设置为需要的 category 标识符,这样系统就知道这个通知对应的是哪个 category。
import UIKit import UserNotifications class ViewController : UIViewController { override func viewDidLoad() { super .viewDidLoad() //设置推送内容 let content = UNMutableNotificationContent () content.title = "hangge.com" content.body = "囤积iPhoneX的黄牛赔到怀疑人生?" //设置通知对应的category标识符 content.categoryIdentifier = NotificationCategory .news.rawValue //设置通知触发器 let trigger = UNTimeIntervalNotificationTrigger (timeInterval: 5, repeats: false ) //设置请求标识符 let requestIdentifier = "com.hangge.testNotification" //设置一个通知请求 let request = UNNotificationRequest (identifier: requestIdentifier, content: content, trigger: trigger) //将通知请求添加到发送中心 UNUserNotificationCenter .current().add(request) { error in if error == nil { print ( "Time Interval Notification scheduled: \(requestIdentifier)" ) } } } override func didReceiveMemoryWarning() { super .didReceiveMemoryWarning() } } |
源码下载:
3,远程推送使用 category
远程推送也可以使用 category,只需要在 payload 报文中添加 category 字段,并指定预先定义的 category id 就可以了。
{ "aps" :{ "alert" :{ "title" : "hangge.com" , "body" : "囤积iPhoneX的黄牛赔到怀疑人生?" }, "sound" : "default" , "badge" :1, "category" : "news" } } |
十二、使用 Notification Service Extension 拦截并修改通知
iOS 10 中添加了两个与通知相关的 extension:Service Extension 和 Content Extension。本文先介绍下前者。
1,基本介绍
- Service Extension 目前只对远程推送的通知有效。
- Service Extension 可以让我们有机会在收到远程推送通知后,展示之前对通知内容进行修改。
通过本机截取推送并替换内容的方式,我们可以实现端到端 (end-to-end) 的推送加密:
我们在服务器推送 payload 中加入加密过的文本,在客户端接到通知后使用预先定义或者获取过的密钥进行解密,然后立即显示。
这样一来,即使推送信道被第三方截取,其中所传递的内容也还是安全的。使用这种方式来发送密码或者敏感信息,对于一些金融业务应用和聊天应用来说,应该是必备的特性。
2,使用说明
(1)首先我们点击”File” -> “New” -> “Target…“,使用 NotificationService 的模板来创建一个 NotificationService。
(2)NotificationService 的模板已经自动为我们生成了一些基本代码,这里对其稍作修改(自动给通知内容后面加上一个小尾巴)。NotificationService 里特别要注意如下两个方法:
1,didReceive
该方法中有一个等待发送的通知请求。我们通过修改这个请求中的 content 内容,然后在限制的时间内将修改后的内容通过调用 contentHandler 返还给系统,就可以显示这个修改过的通知了。
2,serviceExtensionTimeWillExpire
在一定时间内如果没有调用 contentHandler 的话,系统会调用这个方法,来告诉我们时间到了:
- 我们可以什么都不做,这样的话系统便当作什么都没发生,简单地显示原来的通知。
- 或许我们已经设置好了绝大部分内容,只是有很少一部分没有完成。这时我们也可以像例子中这样调用 contentHandler 来显示一个变更到一半的通知。
import UserNotifications class NotificationService : UNNotificationServiceExtension { var contentHandler: (( UNNotificationContent ) -> Void )? var bestAttemptContent: UNMutableNotificationContent ? //我们可以在后台处理接收到的推送,让后传递修改后的的内容给contentHandler进行展示 override func didReceive(_ request: UNNotificationRequest , withContentHandler contentHandler: @escaping ( UNNotificationContent ) -> Void ) { self .contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as ? UNMutableNotificationContent ) if let bestAttemptContent = bestAttemptContent { //给通知内容添加个小尾巴 bestAttemptContent.body = "\(bestAttemptContent.body) 【来自hangge.com】" contentHandler(bestAttemptContent) } } //如果我们获取消息后一段时间内没有调用 contentHandler 的话,系统会调用这个方法 override func serviceExtensionTimeWillExpire() { //如果消息没处理好,我们也将这个没处理完毕的消息进行展示 if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } } |
(3)如果需要调试这个通知扩展类,注意 Target 要选择 NotificationService,然后编译运行时选择我们的程序。
(4)同时 Service Extension 的发布版本要低于设备的版本(比如我手机是 10.3.1,那么这里可以直接设置为 10)。
(5)最后我们在远程通知的 payload 中增加一个 mutable-content 值为 1 的项来启用内容修改(这个一定要有,否则可能会拦截通知失败)。
1234567891011 | { "aps" : { "alert" : { "title" : "最新资讯" , "body" : "2017全国文明城市公布" }, "sound" : "default" , "badge" : 1, "mutable-content" : 1 }, } |
(6)可以看到客户端这边收到通知后,会自动在内容尾部增加一个段小尾巴(【来自hangge.com】)
十三、为本地通知添加多媒体内容
多媒体推送是 iOS10 新增加的一个功能。我们可以在通知中嵌入图片或者视频,这极大地丰富了推送内容的可读性和趣味性。
1,使用说明
- 为本地通知添加多媒体内容十分简单,只需要通过本地磁盘上的文件 URL 创建一个 UNNotificationAttachment 对象,然后将这个对象放到数组中赋值给 content 的 attachments 属性就可以了。
- attachments 虽然是一个数组,但是系统只会展示第一个 attachment 对象的内容。不过我们依然可以发送多个 attachments,然后在要展示的时候再重新安排它们的顺序,以显示最符合情景的图片或者视频。另外,我们也可能会在自定义通知展示 UI 时用到多个 attachment,这个我们下文会进行演示。
- 系统在创建 attachement 时会根据提供的 url 后缀确定文件类型,如果没有后缀,或者后缀不正确的话,我们可以在创建时通过 UNNotificationAttachmentOptionsTypeHintKey 来指定资源类型。
2,多媒体文件的格式、尺寸限制
(1)支持的最大尺寸:
- 图片:10MB
- 音频:5MB
- 视频:50MB
(2)支持的文件格式:
- 图片:kUTTypeJPEG、kUTTypeGIF、kUTTypePNG
- 音频:kUTTypeAudioInterchangeFileFormat、kUTTypeWaveformAudio、kUTTypeMP3、kUTTypeMPEG4Audio
- 视频:kUTTypeMPEG、kUTTypeMPEG2Video、kUTTypeMPEG4、kUTTypeAVIMovie
3,使用样例
(1)下面代码我们给通知附带上一张图片:
import UIKit import UserNotifications class ViewController : UIViewController { override func viewDidLoad() { super .viewDidLoad() //设置推送内容 let content = UNMutableNotificationContent () content.title = "hangge.com" content.body = "囤积iPhoneX的黄牛赔到怀疑人生?" //给通知添加图片附件 if let imageURL = Bundle .main.url(forResource: "image" , withExtension: "png" ), let attachment = try? UNNotificationAttachment (identifier: "imageAttachment" , url: imageURL, options: nil ) { content.attachments = [attachment] } //设置通知触发器 let trigger = UNTimeIntervalNotificationTrigger (timeInterval: 5, repeats: false ) //设置请求标识符 let requestIdentifier = "com.hangge.testNotification" //设置一个通知请求 let request = UNNotificationRequest (identifier: requestIdentifier, content: content, trigger: trigger) //将通知请求添加到发送中心 UNUserNotificationCenter .current().add(request) { error in if error == nil { print ( "Time Interval Notification scheduled: \(requestIdentifier)" ) } } } override func didReceiveMemoryWarning() { super .didReceiveMemoryWarning() } } |
(2)效果图
- 上面代码运行后,在通知显示时,横幅或者弹窗将附带有设置的图片。
- 使用 3D Touch pop 通知或者下拉通知显示详细内容时,图片也会被放大展示。
- 除了图片以外,通知还支持音频以及视频。我们可以将 MP3 或者 MP4 这样的文件提供给系统,从而在通知中进行展示和播放。
4,访问已创建的 attachment 的内容
我们可以访问一个已经创建的 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() } } |
十四、为远程推送添加多媒体内容
1,实现原理
对于远程推送,我们也可以显示图片等多媒体内容。不过需要通过上面介绍的 Notification Service Extension 来修改推送通知内容的技术。具体流程如下:
- 我们在推送的 payload 中指定需要加载的图片资源地址,这个地址可以是应用 bundle 内已经存在的资源,也可以是网络的资源。
- 客户端收到通知后,根据资源地址创建相应的 UNNotificationAttachment。由于只能使用本地资源创建 UNNotificationAttachment,所以如果多媒体还不在本地的话,我们需要先将其下载到本地。
- 在完成 UNNotificationAttachment 创建后,我们就可以像本地通知一样,将它设置给 attachments 属性,然后调用 contentHandler 了。
2,使用样例
(1)假设我们的远程通知 payload 报文如下,其中:
- mutable-content:表示我们会在接收到通知时需要对内容进行更改。
- image:表示需要显示的图片的地址。
{ "aps" : { "alert" : { "title" : "最新资讯" , "body" : "2017全国文明城市公布" }, "sound" : "default" , "badge" : 1, "mutable-content" : 1 }, "image" : "https://img1.gtimg.com/ninja/2/2017/05/ninja149447456097353.jpg" } |
(2)项目这边创建一个 NotificationService,作用是接收到上面这样的通知时会自动提取图片地址、下载,并生成 attachment,然后进行通知展示。
import UserNotifications class NotificationService : UNNotificationServiceExtension { var contentHandler: (( UNNotificationContent ) -> Void )? var bestAttemptContent: UNMutableNotificationContent ? //我们可以在后台处理接收到的推送,让后传递修改后的的内容给contentHandler进行展示 override func didReceive(_ request: UNNotificationRequest , withContentHandler contentHandler: @escaping ( UNNotificationContent ) -> Void ) { self .contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as ? UNMutableNotificationContent ) if let bestAttemptContent = bestAttemptContent { //将远程推送通知中的图片下载到本地,并显示 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: "download" , url: localURL, options: nil ) bestAttemptContent.attachments = [attachment] } catch { print (error) } } contentHandler(bestAttemptContent) } } } } //如果我们获取消息后一段时间内没有调用 contentHandler 的话,系统会调用这个方法 override func serviceExtensionTimeWillExpire() { //如果消息没处理好,我们也将这个没处理完毕的消息进行展示 if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } //将图片下载到本地临时文件夹中 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 timeInterval = Date ().timeIntervalSince1970 let timeStamp = Int (timeInterval) //文件后缀 let ext = (url.absoluteString as NSString ).pathExtension let temporaryURL = FileManager . default .temporaryDirectory let url = temporaryURL.appendingPathComponent( "\(timeStamp)" ) .appendingPathExtension(ext) if let _ = try? data.write(to: url) { localURL = url } } handler(localURL) }) task.resume() } } |
(3)具体效果如下。可以看到即使是远程通知,附带的也是网络图片,但也是可以正常显示的。
原文出自:www.hangge.com 转载请保留原文链接:https://www.hangge.com/blog/cache/detail_1852.html