macOS 开发之本地消息通知

macOS 中的消息推送分为本地消息通知和远程消息通知,本文十里将介绍一下本地消息通知,展示一些常规的使用方法,方便大家了解本地消息推送的实现过程。

开发平台

  • macOS 10.14.3
  • swift 4.2.1
  • xcode 10.1

简单介绍

本地消息通知(Notification)是由 App 请求用户消息中心(User Notifications Center)而推送的,我们的 App 既是消息的提供者又是消息的接受者,一个 App 最多支持 64 个消息通知!

消息通知组成

一个消息通知一般在显示上看由标题(title)、副标题(subtitle)、消息内容、logo和按钮组成。另外还包括提示音的设置和用户信息(用于传递数据)。

消息通知的类型

消息通知主要有三种类型:无、横幅(barner)和提示(alert)

默认情况下使用 横幅 样式!

通知推送的触发条件

消息推送的触发条件有多种方式,消息中心会根据指定的触发条件推送消息:

  • 直接推送通知,一般是根据 App 中自身逻辑灵活手动触发
  • 按照指定时间间隔推送通知,可以设置为重复或者不重复
  • 根据指定的日期时间推送通知,可以设置是否重复
  • 根据地理位置推送通知,通常要设置经纬度坐标以及范围(方圆距离)

消息通知的处理

本地消息通知通过用户消息通知中心的消息队列进行管理,根据通知的触发条件推送的消息会依次存放到这个消息队列,然后依次通知给 App,而 App 可以通过对消息中心的代理实现代理方法对消息通知行为进行相应处理。

消息通知的管理

消息通知的管理由通知中心完成,包括消息通知的注册、删除、推送以及权限。

消息通知的注册

要想让 App 配置好的通知能够按照预期进行推送,必须要将通知注册到通知中心。

消息通知的推送

通知中心会时刻监听着每个消息通知,一旦满足触发条件就会像消息通知队列中推送消息。

消息通知的删除

有时需要关闭已经注册的消息通知的推送活动,通知中心可以将指定消息通知删除,不再监管。

消息通知的权限

在 macOS 的系统偏好设置中可以设置指定应用的通知权限:

这些通知权限在 App 中可以由通知中心进行管理,通常就是消息弹窗和声音播放两点。

关于实现

上面说了本地消息通知的一些基本概念和简单介绍,那么实现本地消息通知的流程就很清楚了:

  • 消息通知的配置,包括触发条件、消息通知中内容
  • 注册消息通知到消息通知中心
  • 实现消息通知中心的代理方法,从而完成对消息的处理

目前看 Apple 官方的开发接口有两套:

  • 传统的消息通知接口,在 Foundation 框架中实现,SDK 支持明确标记 macOS 10.8–10.14 Deprecated,也就是说 10.14 之后便废弃了
  • 最新的消息通知接口,与多个软件平台(iOS、watchOS、tvOS)共用,使用 UserNotifacations 框架,SDK 支持
    • iOS 10.0+
    • macOS 10.14+
    • tvOS 10.0+
    • watchOS 3.0+

不难看出 Apple 的软件开发生态蓝图的宏大,这不在本文讨论范围内!在下面会以一个简单的例子介绍使用两套方法的实现过程,不过会着重讲新的接口!

示例

下面我们通过两个简单的 Demo 看一下如何实现本地消息通知。两个 Demo 中我们分别设置一个按钮,并分别绑定 各自的 Action,一个 Action 中按照传统的方式实现消息通知,另一个 Action 中按照新的方式实现。这个消息通知具有以下实现:

  • 立马推送的
  • 使用系统声音作为提示音,消息推送时播放
  • 通知携带数据在用户信息中
  • 通知设置两个按钮,一个是关闭一个是确定按钮,点击后打印传递的用户信息

传统实现方式

  • 打开 Xcode 新建一个使用 storyboard 的工程,我们就命名为 OldNotificationDemo
  • 打开 Main.storyboard ,在 view controller 中添加一个按钮,按钮标题改为 通知
  • 为两个按钮绑定 action,在 Main.storyboard 中按下快捷键 Option + Command + 回车键 打开辅助编辑器,按住 ctrl 键的同时鼠标左键拖动按钮到 ViewController.swift 的 ViewController 类中绑定 action 为 oldNotificationAction

更改通知样式

传统接口下,App 默认使用横幅的通知样式,但是横幅的通知只能显示 reply button 和 other button,但是我们想自己定义一个按钮,只能使用提示的样式,所以我们首先更改一下通知样式,需要在 Info.plist 文件中添加一个新的键——NSUserNotificationAlertStyle,值设置为 alert:

oldNotificationAction 实现

下面在 oldNotificationAction 中添加使用传统方法消息通知的实现:

@IBAction func oldNotificationAction(_ sender: Any) {
    let userNotification = NSUserNotification()
    
    userNotification.title = "传统方式"
    userNotification.subtitle = "old"
    userNotification.informativeText = "我是一个传统的方式"
    
    userNotification.hasActionButton = true
    userNotification.otherButtonTitle = "关闭"
    userNotification.actionButtonTitle = "显示"
    
    userNotification.identifier = "OLD_NOTIFICATION_DEMO"
    userNotification.userInfo = ["method": "old"]
    
    userNotification.soundName = NSUserNotificationDefaultSoundName
    
    NSUserNotificationCenter.default.delegate = self
    NSUserNotificationCenter.default.deliver(userNotification)
}
  • 上面使用消息通知中心的 deliver 方法直接推送消息,如果要设置其它触发方式的通知需要使用通知中心的 scheduleNotification 方法
  • 将要传递用户数据设置在 userNotificationuserInfo
  • 设置了通知中心的代理为 self,所以要完成剩下的实现,还需要实现代理方法
  • 要显示 action 按钮必须设置 userNotificationhasActionButtontrue

实现代理方法

extension ViewController: NSUserNotificationCenterDelegate {
    
    // 当 App 在前台时是否弹出通知
    func userNotificationCenter(_ center: NSUserNotificationCenter, shouldPresent notification: NSUserNotification) -> Bool {
        return true
    }
    
    // 推送消息后的回调
    func userNotificationCenter(_ center: NSUserNotificationCenter, didDeliver notification: NSUserNotification) {
        print("\(Date(timeIntervalSinceNow: 0)) -> 消息已经推送")
    }
    
    // 用户点击了通知后的回调
    func userNotificationCenter(_ center: NSUserNotificationCenter, didActivate notification: NSUserNotification) {
        switch notification.activationType {
        case .actionButtonClicked:
            let method = notification.userInfo!["method"] as! String
            print("methods -> \(method)")
        case .contentsClicked:
            print("clicked")
        case .replied:
            print("replied")
        case .additionalActionClicked:
            print("additional action")
        default:
            print("action")
        }
    }
    
}
  • 代理方法 userNotificationCenter(:shouldPresent:)->Bool 中如果返回 false,那么 App 的窗口是当前系统桌面显示的窗口,就不会弹出通知也不会播放提示音
  • 通知中心推送消息后会调用 userNotificationCenter(:didDeliver:)
  • 当用户操作弹窗时,比如点击弹窗、点击弹窗上的按钮时,userNotificationCenter(:didActivate) 方法就会被调用,在其中要实现对各种操作的处理

运行程序

运行程序后点击窗口中按钮,不出意外就会看到通知弹窗,同时控制台会打印推送消息,点击弹窗的按钮或弹窗可以看到控制台打印了相应的信息!

Demo 下载:OldNotificationDemo

最新实现方式

  • 打开 Xcode 新建一个使用 storyboard 的工程,我们就命名为 NotificationDemo
  • 打开 Main.storyboard ,在 view controller 中添加一个按钮,按钮标题改为 通知
  • 为两个按钮绑定 action,在 Main.storyboard 中按下快捷键 Option + Command + 回车键 打开辅助编辑器,按住 ctrl 键的同时鼠标左键拖动按钮到 ViewController.swift 的 ViewController 类中绑定 action 为 notificationAction

关于样式

新的实现方式与传统的实现方式不同的是,在样式为**横幅(barner)**时,将鼠标放置在通知弹窗上可以显示自定义的 action 按钮,所以这里没必要更改样式!

notificationAction 实现

因为新的方式使用的是 UserNotifications 框架,所以需要先导入模块,在 ViewController.swift 中添加代码:

import UserNotifications

新的方式通知的实现过程与传统的有很大不同,流程大概是:

  • 先创建一个 UNMutableNotificationContent 来设置通知的内容,包括标题、内容、图片、标识符、提示声音以及用户数据
  • (可选) 创建一个触发器,触发器的类型有很多:
    • UNCalendarNotificationTrigger: 通过指定日期和时间进行触发
    • UNTimeIntervalNotificationTrigger: 通过设置指定时间间隔和是否重复来触发
    • UNLocationNotificationTrigger: 通过指定地理坐标及地域范围来触发
  • (可选) 创建操作集合,这个操作集合类型为 UNNotificationCategory 对应通知弹窗的按钮,集合中元素为 UNNotificationAction 实例,需要调用通知中心的 setNotificationCategories 方法添加生效。
    • barner样式下直接显示两个操作项按钮
    • alert 样式下集合下的操作项会显示为 操作 的子项
    • UNNotificationAction 创建时需要指定唯一标识符、显示名称和选项,标识符用于后期区分 action 进行操作处理
    • 集合的唯一标识符与通知内容实例的唯一标识符统一起来时,才能在 barner 样式下显示按钮
  • 然后通过上面创建好的通知内容实例和触发器创建一个通知请求,它是 UNNotificationRequest 实例,还需要指定一个唯一标识符,另外如果指定的触发器为空,通知中心会立即推送通知
  • 最后指定通知中心的代理实例,一般情况就是类自身即 self,之后调用通知中心实例的 add 方法将通知请求添加到通知中心实例,这个通知中心实例使用系统当前的就可以,调用 UNUserNotificationCenter.current() 即可获得

所以根据我们的实现目标,notificationAction 的实现如下:

@IBAction func notificationAction(_ sender: Any) {
    let content = UNMutableNotificationContent()
    content.title = "新的方式"
    content.body = "我是一个新的方式"
    
    content.userInfo = ["method": "new"]
    
    content.sound = UNNotificationSound.default
    content.categoryIdentifier = "NOTIFICATION_DEMO"
    
    let acceptAction = UNNotificationAction(identifier: "SHOW_ACTION", title: "显示", options: .init(rawValue: 0))
    let declineAction = UNNotificationAction(identifier: "CLOSE_ACTION", title: "关闭", options: .init(rawValue: 0))
    let testCategory = UNNotificationCategory(identifier: "NOTIFICATION_DEMO",
                                              actions: [acceptAction, declineAction],
                                              intentIdentifiers: [],
                                              hiddenPreviewsBodyPlaceholder: "",
                                              options: .customDismissAction)
    
    let request = UNNotificationRequest(identifier: "NOTIFICATION_DEMO_REQUEST",
                                        content: content,
                                        trigger: nil)
    
    // Schedule the request with the system.
    let notificationCenter = UNUserNotificationCenter.current()
    notificationCenter.delegate = self
    notificationCenter.setNotificationCategories([testCategory])
    notificationCenter.add(request) { (error) in
        if error != nil {
            // Handle any errors.
        }
    }
}

注意:

testCategorycontent 一定要使用一致的标识符,否则通知横幅样式下不会显示 action 按钮

代理方法的实现

直接贴出实现:

extension ViewController: UNUserNotificationCenterDelegate {
    
    // 用户点击弹窗后的回调
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo
        switch response.actionIdentifier {
        case "SHOW_ACTION":
            print(userInfo)
        case "CLOSE_ACTION":
            print("Nothing to do")
        default:
            break
        }
        completionHandler()
    }
    
    // 配置通知发起时的行为 alert -> 显示弹窗, sound -> 播放提示音
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.alert, .sound])
    }
}
  • 两个回调中都有一个逃逸闭包参数,是一个完成处理的回调,一定要执行相应的 completionHandler
  • userNotificationCenter(:willPresent:withCompletionHandler) 方法中通过 completionHandler 配置通知行为,这里配置既显示弹窗又播放提示音

关于消息通知权限

其实严格来讲,一个 app 在第一次启动的时候要向系统请求设置通知权限的,等在之后的所有启动的时候就不需要请求设置权限了,只需每次读取系统偏好设置中的权限配置来实现相应的通知行为。貌似在 macOS 中不做这个操作,目前也没什么影响,如果想进一步了解权限可以阅读 Asking Permission to Use Notifications!

验证实现

运行程序,点击窗口中的按钮就能看到通知了!将鼠标放在通知上,就能显示操作按钮,点击按钮就能在 xcode 控制器窗口看到相应的打印信息了。

Demo 下载:NotificationDemo

总结

到目前为止,我们尝试了两种方式实现消息通知的推送,如果有其它的实现需求,比如按日期时间推送消息,直接参考阅读 Apple 官方的资料吧!Apple 的各大系统平台接口融合是大势所趋,如果有必要,大家赶快根据新的实现方式替换马上要淘汰的方法吧!

参考