User Notifications框架

User Notifications框架

User Notifications 是iOS开发中,专门用于给用户发送通知的框架。 用户通知是App与用户交互的一种方式,无论App是否在用户的设备上运行,我们都可以给他们发送通知。比如提醒用户某个需要代办的事项,推送一些天气变化的情况等。

用户通知的分类

用户通知分两种

  1. 本地通知:通过App创建通知,在特定的条件下,比如在特定的时间,或者特定的地理位置,向用户发送通知。
  2. 远程通知:也叫推送通知,由App开发者的远程服务端(Provider Server),先向苹果的Apple Push Notification service (APNs)发送消息,再由APNs把消息发送到用户的设备, 最终以通知的形式展现给用户。

用户通知的时效性和投递率

iOS系统会尽可能地按时发送本地通知和远程通知给用户,但是并不保证通知一定会送达,如果对通知有更高的要求,可以考虑使用PushKit。两种的使用场景区别如下:

  • 使用 User Notifications:适用于绝大多数标准通知场景,如应用内消息、提醒、警告、新闻推送等。适用于希望通知显示在通知中心、并且允许用户与通知互动的应用场景。
  • 使用 PushKit:当开发 VoIP 应用或需要高优先级、立即唤醒应用的推送时,选择 PushKit。例如,呼叫通知、紧急消息或即时通讯应用中的 VoIP 通话。

用户通知的处理步骤

使用用户通知主要分以下几步

  1. 请求用户授权
  2. 定制通知内容
  3. 发送通知
  4. 处理通知

后面将逐一介绍

请求用户授权

用户通知的形式和样式

用户通知会在锁屏界面,通知中心以及Banner位显示,并且伴随着通知的声音和App角标提示。由于用户通知可能会影响用户的交互体验,因此,想要给用户发送通知时,一定要获取用户的授权才可以。

在请求用户授权时,需要明确指明App使用的通知方式,通过 UNAuthorizationOptions , 我们可以知道用户通知的方式有如下几种

  • badge: The ability to update the app’s badge.
  • sound: The ability to play sounds.
  • alert: The ability to display alerts.
  • carPlay: The ability to display notifications in a CarPlay environment.
  • criticalAlert: The ability to play sounds for critical alerts.
  • providesAppNotificationSettings: An option indicating the system should display a button for in-app notification settings.
  • provisional: The ability to post noninterrupting notifications provisionally to the Notification Center.

上面几种类型中,除了provisional表明是设置临时的用户通知(后面会详细介绍到),其它都是控制用户通知的交互方式

用户授权分两种方式

  • 明确请求授权
  • 临时请求授权

明确请求用户授权

通过UNUserNotificationCenter实例的requestAuthorization()方法, 并指定通知的形式,比如alert, badege, sound等,可以发起用户授权

func requestAuthorization() async {
	do {
		if try await center.requestAuthorization(options: [.alert, .badge, .sound]) {
			// You have authorization.
			print("You have authorization.")
		} else {
			// You don't have authorization.
			print("You don't have authorization.")
		}
	} catch {
		// Handle any errors.
		print("error: \(error.localizedDescription)")
	}
}
授权对话框仅会显示一次

当App在第一次执行请求用户授权时,会弹出类似如下的对话框,让用户做选择。

请求授权

一旦用户选择了允许授权或者拒绝授权后,系统会记录下用户的选择结果,以后App代码再次请求用户授权时,不会再弹出上面的请求授权对方框,而是之前返回用户之前选择的结果。除非用户卸载并重新安装了App,不然App都不会再弹出上面的对话框。

因此首次请求用户授权的时机非常重要,一般不建议在用户一打开App的情况下就向用户发起请求,因为这个时候,用户往往不知道App需要授权的目的是什么,很容易直接拒绝授权。最佳的时机是在用户在App中进行了某种操作,需要App通过通知提醒他时,比如:用户添加了一个代办事项,App需要在指定的时间向用户发送通知提醒用户,这种情况下发起用户通知授权的请求,才容易被用户所接受。

临时请求授权

当App明确请求发送通知的权限时,用户必须在看到授权对话框时决定是否允许或拒绝权限。就算我们考虑了请求对话框的弹出时机,但用户仍然因为某些顾虑,会拒绝授权。

这种情况下可以考虑使用临时授权来临时发送通知, 临时授权不会弹出请求授权的对话框给用户,让用户做选择,但当收到通知时,也不会响起通知声音,在锁屏界面或者Banner位显示通知消息,而是仅出现在通知中心的历史记录中。这些通知还包含按钮,提示用户保留关闭通知。如果用户按下“关闭”按钮,系统会在确认选择后拒绝应用发送更多的通知。

如果用户按下“保留”按钮,根据用户是否在系统设置的通知中开启了”Scheduled Summary”功能,提示给用户的信息也不一样。

假如用户之前在系统设置的通知中开启了”Scheduled Summary”功能,系统会提示用户在两种选项之间做出选择:立即发送在计划摘要中发送

选择“立即发送”后,未来的通知会安静地传递。系统会授权你的应用发送通知,但不会授予显示警报、播放声音或为应用图标添加标记的权限。通知只会出现在通知中心的历史记录中,除非用户更改通知设置。

假如用户之前在没有在系统设置的通知中开启”Scheduled Summary”功能,则只会显示”立即发送”。

相比明确请求用户授权,请求临时授权时,只需要在请求发送通知权限时添加provisional选项就可以了。

func requestAuthorization() async {
	do {
		if try await center.requestAuthorization(options: [.alert, .badge, .sound, .provisional]) {
			// You have authorization.
			print("You have authorization.")
		} else {
			// You don't have authorization.
			print("You don't have authorization.")
		}
	} catch {
		// Handle any errors.
		print("error: \(error.localizedDescription)")
	}
}

与明确请求授权不同,这个代码不会弹出请求授权的对话框给用户,让用户选择。相反,当第一次调用此方法时,系统会自动授予权限。然而,直到用户明确选择保留或关闭通知之前,授权状态将一直保持为 UNAuthorizationStatus.provisional

此外,如果应用是请求临时授权,则可以在应用首次启动时请求授权。不用考虑请求的时机问题,因为用户只有在实际收到通知时,才会被在通知中心中被要求选择保留或关闭通知,并且不会影响到用户。

获取用户的授权情况

由于用户可能随时会调整App的通知权限,因此,在每次发送通知前,都最好先检查一下当前用户是否允许发送通知。通过UNUserNotificationCenter实例的notificationSettings()方法, 可以检查当前用户是否授权。

let center = UNUserNotificationCenter.current()

// Obtain the notification settings.
let settings = await center.notificationSettings()

// Verify the authorization status.
guard (settings.authorizationStatus == .authorized) ||
      (settings.authorizationStatus == .provisional) else { return }

if settings.alertSetting == .enabled {
    // Schedule an alert-only notification.
} else {
    // Schedule a notification with a badge and sound.
}

通知示例

先看一个完整的发送本地通知的示例,至于发送通知的细节,我们后面会讲到。发送通知后,记得将App置于后台或者锁屏,等待5秒钟,既可以看到通知效果。

import SwiftUI
import UserNotifications

struct LocalNotification: View {
    let center = UNUserNotificationCenter.current()
    let authorizationOptions: UNAuthorizationOptions = [.alert, .badge, .sound]

    var body: some View {
        VStack(alignment: .leading) {
            VStack(alignment: .leading) {
                Button("Request Explicitly Notification Permission") {
                    Task {
                        await requestAuthorization(options: authorizationOptions, provisional: false)
                    }
                }

                Button("Send Explicitly Notification") {
                    Task {
                        await sendNotification(provisional: false)
                    }
                }
            }
            .padding()
            .border(.black)

            VStack(alignment: .leading) {
                Button("Request Provisional Notification Permission") {
                    Task {
                        await requestAuthorization(options: authorizationOptions, provisional: true)
                    }
                }

                Button("Send Provisional Notification") {
                    Task {
                        await sendNotification(provisional: true)
                    }
                }
            }
            .padding()
            .border(.black)

            Spacer()
        }
        .buttonStyle(.borderedProminent)
    }

    func createRequest() -> UNNotificationRequest {
        let content = UNMutableNotificationContent()
        content.title = "Notification Title"
        content.subtitle = "Notification Subtitle"
        content.sound = UNNotificationSound.default

        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
        return UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
    }

    func requestAuthorization(options: UNAuthorizationOptions, provisional: Bool = false) async {
        do {
            var options = options
            if provisional {
                options = options.union(.provisional)
            }
            if try await center.requestAuthorization(options: options) {
                // You have authorization.
                print("You have authorization.")
            } else {
                // You don't have authorization.
                print("You don't have authorization.")
            }
        } catch {
            // Handle any errors.
            print("error: \(error.localizedDescription)")
        }
    }

    func sendNotification(provisional: Bool) async {
        let settings = await center.notificationSettings()
        if settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional {
            print("authorized: \(settings.authorizationStatus)")
//            if settings.alertSetting == .enabled {
//                // Schedule an alert-only notification.
//            } else {
//                // Schedule a notification with a badge and sound.
//            }
            await addNotification()
        } else {
            print("未授权 \(settings.authorizationStatus)")
            await requestAuthorization(options: authorizationOptions, provisional: provisional)
        }
    }

    func addNotification() async {
        do {
            try await center.add(createRequest())
        } catch {
            print("add request failed, err: \(error.localizedDescription)")
        }
    }
}