在纯SwiftUI App中实现Push功能

875 阅读3分钟

Push处理有以下场景

  1. App在前台接收到通知
  2. App在后台接收到通知
  3. 用户通过点击通知调起App

设置

  1. 由于纯SwiftUI App 的入口是一个struct,而我们需要使用的UserNotifications是一个OC框架,所以我们需要创建一个类去遵循UIApplicationDelegate
import UIKit
import UserNotifications

class MyAppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
    ) -> Bool {
        // 注册通知代理处理即将到来的通知
        UNUserNotificationCenter.current().delegate = self

        // 请求通知权限
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current()
            .requestAuthorization(
                options: authOptions,
                completionHandler: { success, error in
                    // 处理用户授权的状态
                    if success {
                        print("用户已经授权")
                    } else {
                        // 这里需要根据错误重新提示用户授权或做一些其他处理。目前只是打印错误
                        print(error?.localizedDescription as Any)
                    }
                }
            )
        return true
    }
}

// MARK: - UNUserNotificationCenterDelegate
extension MyAppDelegate: UNUserNotificationCenterDelegate {
    // 如果App在前台接收到通知
    func userNotificationCenter(_ center: UNUserNotificationCenter,

                                willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
          return [.list, .banner, .sound]
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter,

                                didReceive response: UNNotificationResponse) async {
        // 处理接收到的通知
    }
}
  1. 需要将代理放置到我们的SwiftUI中,这可以通过使用@UIApplicationDelegateAdaptor 完成
// MARK: - AppDelegate
/// 添加代理
@UIApplicationDelegateAdaptor var appDelegate: MyAppDelegate

// MARK: - Body
var body: some Scene {
    WindowGroup {
        ContentView()
    }
    .modelContainer(sharedModelContainer)
}

给App添加Push能力

在Target的 "Signing & Capabilities" 中点击 "+ Capability" 然后输入push就可以 image.png

通知处理

在SwiftUI中已经有onOpenURL(perform:) .onContinueUserActivity(_:perform:)这类的ViewModifier,我们需要尽量适配这些ViewModifier,可以自定义一个ViewModifier,它的签名是onNotification(perform closure: @escaping (UNNotificationResponse) -> Void

为了处理通知,我们需要处理以下事情

接收到来的通知

通过设计一个类来专门处理通知,利用Combine框架完成订阅与通知逻辑

import Combine
import UserNotifications

public class NotificationHandler: ObservableObject {
    // MARK: - Shared Instance
    public static let shared = NotificationHandler()
    private init() {}

    // MARK: - Properties
    /// 最新的通知
    @Published private(set) var latestNotification: UNNotificationResponse? = .none

    // MARK: - Methods
    /// 处理通知并转发到App
    /// - Parameter notification: 需要处理的通知响应
    public func handle(notification: UNNotificationResponse) {
        // 持有响应并通过Combine发布出去
        self.latestNotification = notification
    }
}

之后我们就可以在之前的通知代理里面使用该类处理通知

// MARK: - UNUserNotificationCenterDelegate
extension MyAppDelegate: UNUserNotificationCenterDelegate {
    // 如果App在前台接收到通知
    func userNotificationCenter(_ center: UNUserNotificationCenter,

                                willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
          return [.list, .banner, .sound]
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter,

                                didReceive response: UNNotificationResponse) async {
        // 处理接收到的通知
        NotificationHandler.shared.handle(notification: response)
    }
}

通知到SwiftUI的View

SwiftUI中大多数的事件和UI控制都是通过ViewModifier来处理的,我们要创建一个ViewModifier来贴合SwiftUI的风格

import SwiftUI
typealias NotificationResponseAction = (UNNotificationResponse) -> Void

struct NotificationViewModifier: ViewModifier {
    // MARK: - Private Properties
    private let onNotification: NotificationResponseAction

    // MARK: - Initializers
    // 提供一个闭包,可以让调用者处理自己的通知逻辑
    init(onNotification: @escaping NotificationResponseAction) {
        self.onNotification = onNotification
    }

    // MARK: - Body
    func body(content: Content) -> some View {
        content
        // 订阅通知更新
        .onReceive(NotificationHandler.shared.$latestNotification) { notification in
                guard let notification else { return }
                onNotification(notification)
            }
    }
}

extension View {
    func onNotification(perform action: @escaping NotificationResponseAction) -> some View {
        modifier(NotificationViewModifier(onNotification: action))
    }
}

我们创建了一个ViewModifier并提供了一个便利的使用方法

之后我们就可以在View上使用这个ViewModifier了

// MARK: - Body
var body: some Scene {
    WindowGroup {
        ContentView()
        .onNotification { notification in
            print(notification)
        }
    }
    .modelContainer(sharedModelContainer)
}

保证我们在App前台、后台、以及通过通知启动App时都能调用到

  1. 由于NotificationHandler良好的设计,在用户通过通知启动App,handler就已经处理通知,并保存了latestNotification。
  2. 此时SwiftUI视图还未创建,等View创建时通过onNotification订阅通知时就能接收到latestNotification这个值了

资料

alexanderweiss.dev/blog/2023-0…