Swift Notes - iOS10下的Client端APNS实现(基本)

一晃都 iOS 都到版本10了(明明都10好久了好吧),按照苹果公司没事儿就要搞个大新闻的尿性,肯定又是一大堆的类被精炼,一大堆的方法被废弃,这不到 iOS10了,APNS就倒了霉了(UserNotifications 库 被引入以全盘接管苹果消息推送服务),各种方法被废弃,导致了作者被测试组同事疯狂骚扰,无限报bug(什么坐着收不到通知啦,躺着也收不到啦,什么badge number不对啊。。。)。 于是作者就给掰扯掰扯,就当抛砖了。
新任务获得:升级APNS前端代码以支持全版本下的消息推送需求

消息推送需求肢解

呐,基础的 APNS 组成部件和流程你们肯定都懂,不懂得就随便去网上找下吧解说的不要太多;然后那些证书啊,APP Key啊的配置估计你们也都知道,不知道的可以看下我写的第一篇博客里除了前端代码的那一部分Swift Notes - 阿里云推送SDK配置,当然如果你选用的是其他服务器比如极光,百度之类的,也可以凑合看下,毕竟只是sdk换了下。

那回到前端,我们要的活儿有哪些呢

  1. 先是注册设备 – (把 deviceId 发送给推送服务器,得到 push token作为设备标识以供苹果通知服务器定向推送消息)
  2. 注册请求成功/失败的处理 – (没啥大用,就是成功了么就把 push token 打出来看下,失败了么就把错误信息打出来看看原因)
  3. 那注册完了么自然就是收到通知后的处理了 – (这里我们仅处理标准通知,威力加强版的我们到时候再说,可能下期吧)

    那明确了需求细节之后么,我就开始吧
    2

注册设备

如上所诉,这里的注册有两部,一步启动推送SDK完成初始化;另一步与苹果通知服务器交互完成初始化

推送SDK初始

这个还是和跳的推送服务器有关的,毕竟每家的接口不一样么,详细的和推送服务器初始化交互的接口怎么用,各家SDK肯定都有教程的,这个需要看官们找一下,我这就以阿里云的为例了

// init Aliyun Push SDK
func initCloudPushSDK() {
    CloudPushSDK.asyncInit(testAppKey, appSecret: testAppSecret) { (res) in
        if (res!.success) {
            print("Push SDK init success, deviceId: \(CloudPushSDK.getDeviceId()!)")
        } else {
            print("Push SDK init failed, error: \(res!.error!).")
        }
    }
}

苹果通知服务器交互

那原本的注册代码只要针对 iOS8以下和iOS8以上,写两套代码的(iOS8时苹果公司也搞了个大新闻),那现在引入了 UserNotifications Framework, 我们需要现在AppDelegate文件里引入这个库文件

import UIKit
// iOS 10+ notificaiton
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

然后把iOS8以上版本代码进一步分化为iOS8-10之间和iOS10以上两套代码,总计三套代码如下:

// Register APNS, fetch deviceToen for pushing notifications
func registerAPNs(_ application: UIApplication) {
    if #available(iOS 10, *) {
        // iOS 10+
        let center = UNUserNotificationCenter.current()
        center.delegate = self
        // request User authorization
        center.requestAuthorization(options: [.alert, .badge, .sound], completionHandler: { (granted, error) in
            if (granted) {
                // User authored notification
                print("User authored notification.")
                // Real register to APNS, after user does authorize
                application.registerForRemoteNotifications()
            } else {
                // User denied notification
                print("User denied notification.")
            }
        })
    } else if #available(iOS 8, *) {
        // iOS 8+
        application.registerUserNotificationSettings(UIUserNotificationSettings.init(types: [.alert, .badge, .sound], categories: nil))
        application.registerForRemoteNotifications()
    } else {
        // < iOS 8
        application.registerForRemoteNotifications(matching: [.alert,.badge,.sound])
    }
}

注册设备成功/失败回调

呃,这块貌似没啥好说的了,直接上代码吧

//APNS register success
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    CloudPushSDK.registerDevice(deviceToken as Data!) { (res) in
        if (res?.success)!{
            print("Register deviceToken success")
            print("token\(CloudPushSDK.getApnsDeviceToken())")
        }else{
            print("Register deviceToken failed, error:\(res?.error?.localizedDescription)")
        }
    }
}

//APNS register failed
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
    print("did fail to register for remote notification with error : \(error.localizedDescription)")
}

处理收到的通知

根据前端app所处的状态,我们需要处理三种状态下的通知:Foreground mode Background mode Inactive mode

Foreground Mode

这种模式是app正在前端运行中的状态,设备顶部不会有通知栏出现,所以不会有通知栏手势响应的情况,更多的时候我们是以此触发一些内部逻辑(动画啊,页面跳转什么的),那在iOS10以前,是如下处理的:
注:即便app处于前台,iOS10以后,通知栏也会出现,手势响应同Background Mode(第二段代码)

    //Handle remote Notifications - Foreground Mode (iOS < 10)
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
    print("REceive one notification.")
    if application.applicationState == UIApplicationState.active{

        ...Your logic...
        ...Foreground Mode...

    }
}

如上所说,iOS10引入了UserNotification Framework,自然这个新的库会提供新的方法咯,如下:

//Handle remote Notifications - Foreground Mode (iOS 10+)
@available(iOS 10.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    print("Receive a notification in foreground.")

    ...Your logic...
    ...Foreground Mode...

    // hide alert
    completionHandler([])
    // show alert with popup,badge,sound
    //completionHandler([.alert, .badge, .sound])
}

Background Mode

那这种模式是app依然在运行,只不过是在后台挂起的状态,应该是通知推送最常遇到的场景了,会有顶部弹出,以及通知栏手势响应. 这种模式下的响应函数与Foreground Mode的一样,只不过通过application.status属性进行区分。在iOS10以前,是如下处理的:

//Handle remote Notifications - Foreground Mode (iOS < 10)
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
    print("REceive one notification.")
    if application.applicationState == UIApplicationState.active{

        ...Your logic...
        ...Foreground Mode...

    }else{

        ...Your logic...
        ...Background Mode...

    }
}

iOS10 UserNotifications Framework 被引入以后,上面的代码就瞎了。。。不会被调用,也没有新的替换方法(作者就是个废物啊,确实没研究出来,有知道的看官请告诉我). 但实际上问题也不大啦,毕竟Background Mode下你也没有太多的内部逻辑可动,而且具体的通知手势响应回调函数,UserNotifications Framework 还是厚道的提供了的,如下:

// Handle remote Notifications action(Click/Delete/Self-defined actions) 
//- Foreground & Background Mode (iOS 10+)
@available(iOS 10, *)
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

    if UIApplication.shared.applicationState.applicationState == UIApplicationState.active{

        ...Your logic...
        ...Foreground Mode...

    }else{

        let userAction = response.actionIdentifier
        if userAction == UNNotificationDefaultActionIdentifier {
        print("User opened the notification.")

            ...Your logic...
            ...Background Mode...

          }
         if userAction == UNNotificationDismissActionIdentifier {
            print("User dismissed the notification.")
        }  
    }        
    completionHandler()
}

Inactive Mode

这种模式是app已经被杀掉的状态(挂起过长被动杀掉,手动杀掉),会有顶部通知栏,有通知手势响应。与Foreground ModeBackground Mode不同的是,这种模式走的application: didFinishLaunchingWithOptions方法,而且所有的iOS版本都只有一套代码如下:

//Handle remote Notifications
//- Inactive Mode (iOS all version)
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    if launchOptions != nil {
        let option = launchOptions![UIApplicationLaunchOptionsKey.remoteNotification] as? NSDictionary
        if option != nil {

           ...Your logic...
           ...Inactive Mode...

        }
    }
}

注意事项

角标的问题

大多数时候呢,通知的角标数量都是由后台通过通知内容(Notification Payload)里的Badge字段配置的,前端只是由系统自动的简单的响应下。当然你也可以在适当的地方通过如下代码手动管理:

UIApplication.shared.applicationIconBadgeNumber = 99

这里有的看客可能会问了,那app在Inactive Mode甚至Background Mode下基本无发运行代码,怎么会更新角标(包括播放音乐)?这里同样要求你后端的同事通过将通知内容(Notification Payload)里的Content-avaiable字段配置为1,这样设备就会在收到通知时短暂运行一下app,以实现更新。

拓展

前文有提过新版本下的通知可以玩的花活儿越来越多了,主要有如下两支,至于具体怎么使用呢,我们下回再说(让作者先研究研究)

  • 自定义通知行为(Actionable Notification)
  • 通知拓展(Notification Extension)

3


This artical is avaliable under WTFPL-V2. Generally, everyone is permitted to copy and do what the fuck you want to.
P.S. Even so said, your kindly declaration that inspired from this site - Chen’s Alchemy would be appreciated

本文链接:http://yoursite.com/2017/05/04/swift-APNS-iOS10/

Donate comment here