今天看了一篇有关iOS消息推送的文章 原文
开发环境:
** Xcode 8.3
Swift 3.1 **
随着iOS版本的不断提高,iOS的消息推送也越来越强大,也越容易上手了。 在iOS10中,消息推送的功能有:
- 显示文本信息
- 播放通知声音
- 设置 badge number
- 用户不打开应用的情况下提供actions(下拉推送消息即可看到)
- 显示一个媒体信息
- 在 silent 的情况让应用在后台执行一些任务
测试iOS APNs 的时候,需要用到的:
- 一台iOS设备,因为在模拟器是不行的
- 一个开发者账号(配置APNs的时候需要证书)
在本文中,使用 Pusher 扮演向iOS设备推送消息的服务器的角色,在本文的测试中你可以 直接下载Pusher
iOS的消息推送中,有三个主要步骤:
- app配置注册APNS
- 一个Server推送消息给设备
- app接送处理APNs
1 和 3 主要是iOS开发者干的事情, 2 是消息推送服务端,国内可以用一些第三方的比如 极光推送之类的,当然公司也可以自己配制。
开始之前,下载初始化的项目 starter project, 打开运行,如图所示:
配置App
- 打开应用,在 General 改写 Bundler Identifier 一个唯一的标示 如: com.xxxx.yyyy
- 选择一个开发者账号
- 在Capabilities打开APNS:
注册APNs
在 AppDelegate.swift顶部导入:
import UserNotifications
添加方法:
func resgierForPushNotifications() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
print("Permision granted: \(granted)")
}
}
在方法 application(_:didFinishLaunchingWithOptions:):中调用 registerForPushNotifications()
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
UITabBar.appearance().barTintColor = UIColor.themeGreenColor
UITabBar.appearance().tintColor = UIColor.white
resgierForPushNotifications()
return true
}
** UNUserNotificationCenter**是在iOS10才又的,它的作用主要是在App内管理所有的通知活动
**requestAuthorization(options:completionHandler:) **认证APNs,指定通知类型,通知类型有:
- .badge 在App显示消息数
- .sound 允许App播放声音
- ** .alert** 推送通知文本
- .carPlay CarPlay环境下的消息推送
运行项目,可看到:
点击 Allow 运行推送
在 AppDelegate:中添加方法:
func getNotificationSettings() {
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
print("Notification setting: \(settings)")
}
}
这个方法主要是查看用户允许的消息推送类型,因为用户可以拒绝消息推送,也可以在手机的设置中更改推送类型。
在 ** requestAuthorization **中调用 getNotificationSettings()
func resgierForPushNotifications() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
print("Permision granted: \(granted)")
guard granted else { return }
self.getNotificationSettings()
}
}
更新 getNotificationSettings()方法 如下:
func getNotificationSettings() {
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
print("Notification setting: \(settings)")
guard settings.authorizationStatus == .authorized else { return }
UIApplication.shared.registerForRemoteNotifications()
}
}
** settings.authorizationStatus == .authorized** 表明用户允许推送,** UIApplication.shared.registerForRemoteNotifications()**,实际注册APNs
添加下面两个方法,它们会被调用,显示注册结果:
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenParts = deviceToken.map { data -> String in
return String(format: "%02.2hhx", data)
}
let token = tokenParts.joined()
print("Device Token: \(token)")
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Failed to register: \(error)")
}
如果注册成功,调用: ** application(_:didRegisterForRemoteNotificationsWithDeviceToken:)**
注册失败,调用: ** application(_:didRegisterForRemoteNotificationsWithDeviceToken:)**
在 application(_:didRegisterForRemoteNotificationsWithDeviceToken:)** 方法内做的事情只是简单的把 Data 转换为 String
(一般情况如果注册失败可能是:在模拟器运行 或者 App ID 配置失败,所以输出错误信息查看原因就很重要了 )
编译运行,可看到输出一个类似于这样的字符串:
把输出的 Device Token 拷贝出来,放到一个地方,等一下配置的时候需要这货
创建 SSL 证书 和 PEM 文件
在苹果官网的开发者账号中 步骤:Certificates, IDs & Profiles -> Identifiers -> App IDs 在该应用的 ** App IDs**中应该可以看到这样的信息:
点击 Edit 下拉到 Push Notifications:
在 Development SSL Certificate, 点击 **Create Certificate… **跟随步骤创建证书,最后下载证书,双击证书, 证书会添加到 Keychain
回到开发者账号, 在应用到 **App ID ** 应用可以看到:
到这一步就OK了,你已经有了APNs 的证书了
推送消息
还记得刚才下载的** Pusher **吗? 用那货来发送推送消息
打开 Pusher,完成以下步骤:
- 在 Pusher 中选择刚刚生成的证书
- 把刚刚生成的 Device Token 拷贝进去 (如果你忘了拷贝生成的 Device Token,把应用删除,重新运行,拷贝即可)
- 修改 Pusher 内的消息体如下:
{
"aps": {
"alert": "Breaking News!",
"sound": "default",
"link_url": "https://raywenderlich.com"
}
}
- 应用推到后台,或者锁定
- 在 Pusher 中点击 Push 按钮
你的应用应该可以接受到首条推送消息:
(如果你的应用在前台,你是收不到推送的,推到后台,重新发送消息)
推送的一些问题
一些消息推送接收不到: 如果你同时发送多条推送,而只有一些收到,很正常!APNS 为每一个设备的App维护一个 QoS (Quality of Service) 队列. 队列的size是1,所以如果你同时发送多条推送,最后一条推送是会被覆盖的
连接 Push Notification Service 有问题: 一种情况是你用的 ports 被防火墙墙了,另一种情况可能是你的APNs证书有错误
基本推送消息的结构
更新中...
在目前的推送测试中,消息体是这样的:
{
"aps": {
"alert": "Breaking News!",
"sound": "default",
"link_url": "https://raywenderlich.com"
}
}
下面来分析一下
在 "aps" 这个key 的value中,可以添加 6 个key
- alert. 可以是字符串,亦可以是字典(如果是字典,你可以本地化文本或者修改一下通知的其它内容)
- badge. 通知数目
- thread-id. 整合多个通知
- sound. 通知的声音,可以是默认的,也可以是自定义的,自定义需要少于30秒和一些小限制
- content-availabel. 设置value 为 1 的时候, 该消息会成为 silent 的模式。下午会提及
- category. 主要和 custom actions 相关,下午会有介绍
记得 payload 最大的为 4096 比特
处理推送消息
处理推送过来的消息(使用 actions 或者 直接点击消息 )
当你接收了一个推送消息的时候会发生什么
当接收到推送消息的时候, UIApplicationDelegate 内的代理方法会被调用,调用的情况取决于目前 app 的状态
- 如果 app 没运行,用户点击了推送消息,则推送消息会传递给 ** application(_:didFinishLaunchingWithOptions:).** 方法
- 如果 app 在前台或者后台则 ** application(_:didReceiveRemoteNotification:fetchCompletionHandler:) ** 方法被调用。如果用户通过点击推送消息的方式打开 app , 则 该方法可能会被再次调用,所以你可以依次更新一些UI信息或数据
处理 app 没运行的消息推送情况:
在** application(_:didFinishLaunchingWithOptions:).** 方法的 return 返回前加入:
if let notification = launchOptions?[.remoteNotification] as? [String: AnyObject] {
if let aps = notification["aps"] as? [String: AnyObject] {
_ = NewsItem.makeNewsItem(aps)
(window?.rootViewController as? UITabBarController)?.selectedIndex = 1
}
}
它会检测 ** UIApplicationLaunchOptionsKey.remoteNotification.** 是否存在于 ** launchOptions **,编译运行,把应用结束运行关掉。用 Pusher发生一条推送消息,点击推送消息,应该会显示如图:
(如果没有接收到推送消息,可能是你的设备的 device token 改变了,在未安装 app 或者重新安装 app 的情况下,device token 会改变)
处理 app 运行在前台或者后台的消息推送
application(_:didReceiveRemoteNotification:fetchCompletionHandler:) 方法 更新如下
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let aps = userInfo["aps"] as! [String: AnyObject]
_ = NewsItem.makeNewsItem(aps)
}
方法的做的主要是把推送消息直接加入 NewsItem (把推送消息显示在视图内),编译运行,保持应用在前台或者后台,发生推送消息,显示如下:
好了,现在 app 能给接收推送消息了!
为 “推送通知“ 添加 Actions
为 “推送通知“ 添加 Actions 可以为消息添加一些自定义的按钮。actions 的添加通过在 app 内为通知注册 ** categories** ,每一个 category 可以有自己的 action。一旦注册,推送的服务端可以设置消息的 category。
在示例中,会定义一个名为 ** News** category 和一个相对应的名为 View 的 action,这个 action 会让用户选择这个 action 后直接打开文章
在 AppDelegate 内,替换 **registerForPushNotifications() ** 如下
func registerForPushNotifications() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
(granted, error) in
print("Permission granted: \(granted)")
guard granted else { return }
// 1
let viewAction = UNNotificationAction(identifier: viewActionIdentifier,
title: "View",
options: [.foreground])
// 2
let newsCategory = UNNotificationCategory(identifier: newsCategoryIdentifier,
actions: [viewAction],
intentIdentifiers: [],
options: [])
// 3
UNUserNotificationCenter.current().setNotificationCategories([newsCategory])
self.getNotificationSettings()
}
}
代码干的事为:
// 1. 创建一个新的 notification action, 按钮标题为 View , 当触发时,app 在 foreground(前台)打开. 这个 ation 有一个 identifier, 这个 identifier 是用来标示同一个 category 内的不同 action 的
// 2. 定义一个新的 category . 包含刚刚创建的 action, 有一个自己的 identifier
// 3. 通过 ** setNotificationCategories(_:) ** 方法注册 category.
编译运行 app。
替换 Pusher 内的推送消息如下:
{
"aps": {
"alert": "Breaking News!",
"sound": "default",
"link_url": "https://raywenderlich.com",
"category": "NEWS_CATEGORY"
}
}
如果一切运行正常,下拉推送消息,显示如下:
nice, 点击 View, 打开 app,但是什么都没有发生,你还需要实现一些代理方法来处理 action
处理通知的 Actions
当 actions 被触发的时候, UNUserNotificationCenter 会通知它的 delegate。在 AppDelegate.swift 内添加如下 extension
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
// 1
let userInfo = response.notification.request.content.userInfo
let aps = userInfo["aps"] as! [String: AnyObject]
// 2
if let newsItem = NewsItem.makeNewsItem(aps) {
(window?.rootViewController as? UITabBarController)?.selectedIndex = 1
// 3
if response.actionIdentifier == viewActionIdentifier,
let url = URL(string: newsItem.link) {
let safari = SFSafariViewController(url: url)
window?.rootViewController?.present(safari, animated: true, completion: nil)
}
}
// 4
completionHandler()
}
}
方法代码主要做的是判断 action 的 identifier, 打开推送过来的 url。
在 application(_:didFinishLaunchingWithOptions:): 方法内,设置 ** UNUserNotificationCenter** 的代理
UNUserNotificationCenter.current().delegate = self
编译运行,关掉 app, 替换推送消息如下:
{
"aps": {
"alert": "New Posts!",
"sound": "default",
"link_url": "https://raywenderlich.com",
"category": "NEWS_CATEGORY"
}
}
下拉推送消息,点击 View action, 显示如下:
现在 app 已经能够处理 action 了, 你也可以定义自己的 action 试一试。
Silent 推送消息
Silent 推送消息可以 在后台默默的唤醒你的 app 去执行一些任务. WenderCast 可以使用它来更新 podcast list.
到 App Settings -> Capabilites 为 WenderCast 打开 Background Modes . 勾选 Remote Notifications
现在 app 在接收到这类消息的时候就会在后台唤醒。
在 ** AppDelegate** 内,替换 ** application(_:didReceiveRemoteNotification:) ** 如下:
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let aps = userInfo["aps"] as! [String: AnyObject]
// 1
if aps["content-available"] as? Int == 1 {
let podcastStore = PodcastStore.sharedStore
// Refresh Podcast
// 2
podcastStore.refreshItems { didLoadNewItems in
// 3
completionHandler(didLoadNewItems ? .newData : .noData)
}
} else {
// News
// 4
_ = NewsItem.makeNewsItem(aps)
completionHandler(.newData)
}
}
代码干的事为:
// 1 判断 content-available 是否为 1 来确定是否为 Silent 通知
// 2 异步更新 podcast list
// 3 当更新完以后,调用 completionHandler 来让系统确定是否有新数据载入了
// 4 如果不是 silent 通知,假定为普通消息推送
确定调用 completionHandler 的时候传入真实的数据,系统会依次判断电池在后台运行的消耗情况,系统会在需要的时候可能会把你的 app 杀掉。
替换 Pusher 如下:
{
"aps": {
"content-available": 1
}
}
如果一切正常,你是看不到什么的,当然你也可以直接加入一些print方法,查看控制台输出情况 看是否执行了, 比如替换如下:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let aps = userInfo["aps"] as! [String: AnyObject]
if aps["content-available"] as? Int == 1 {
print("=== content-available")
let podcastStore = PodcastStore.sharedStore
podcastStore.refreshItems({ (didLoadNewItems) in
completionHandler(didLoadNewItems ? .newData : .noData)
})
} else {
print("=== no, content-availabel")
_ = NewsItem.makeNewsItem(aps)
completionHandler(.newData)
}
}
原文查看是否运行的方法是:
打开scheme:
** Run -> Info** 选择 Wait for executable to be launched:
在 application(_:didReceiveRemoteNotification:fetchCompletionHandler:) 方法内,打断点看是否运行。
最后
你也可以下载 完成代码 看运行情况,当然需要做的是修改 Bundle ID, 替换自己的证书。
虽然 APNs 对于 app 来说很重要,但是如果发生太频繁的推送消息,用户很可能会把 app 卸载掉,所以还是要合理发生推送消息。