在自己的应用程序/视图中接收本地通知(或如何在 SwiftUI 中注册 UNUserNotificationCenterDelegate )

receive local notifications within own app / view (or how to register a UNUserNotificationCenterDelegate in SwiftUI )

我正在使用包含倒计时功能的 SwiftUI 为 iOS 重新开发一个 android 应用程序。当倒计时结束时,应该通知用户倒计时结束。 Notification 应该具有一定的侵入性,并且适用于不同的场景,例如当用户未主动使用 phone 时,当用户正在使用我的应用程序时以及当用户正在使用其他应用程序时。我决定使用本地通知来实现这一点,这是 android 的工作方法。 (如果这种方法完全错误,请告诉我什么是最佳实践)

但是,当用户当前正在使用我的应用程序时,我无法收到通知。通知只显示在消息中心(所有通知排队的地方),但不会主动弹出。

到目前为止,这是我的代码: 要求用户获得在我的 CountdownOrTimerSheet 结构中使用通知的权限(从不同的视图作为 actionSheet 调用):

/**
    asks for permission to show notifications, (only once) if user denied there is no information about this , it is just not grantedand the user then has to go to settings to allow notifications 
    if permission is granted it returns true
 */
func askForNotificationPermission(userGrantedPremission: @escaping (Bool)->())
{
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
        if success {
            userGrantedPremission(true)
        } else if let error = error {
            userGrantedPremission(false)
        }
    }
}

仅当用户允许通知权限时,我的 TimerView 结构才会被调用

                         askForNotificationPermission() { (success) -> () in
                            
                            if success
                            {
                                
                                // permission granted

                                ...
                                // passing information about the countdown duration and others..
                                ...
                                
                                userConfirmedSelection = true // indicates to calling view onDismiss that user wishes to start a countdown
                                showSheetView = false // closes this actionSheet
                            }
                            else
                            {
                                // permission denied
                                showNotificationPermissionIsNeededButton = true
                            }
                        }

来自上一个视图

                   .sheet(isPresented: $showCountDownOrTimerSheet, onDismiss: {
                        // what to do when sheet was dismissed
                        if userConfirmedChange
                        {
                            // go to timer activity and pass startTimerInformation to activity
                            programmaticNavigationDestination = .timer
                            
                        }
                    }) {
                        CountdownOrTimerSheet(startTimerInformation: Binding($startTimerInformation)!, showSheetView: $showCountDownOrTimerSheet, userConfirmedSelection: $userConfirmedChange)
                    }


                    ...


                    NavigationLink("timer", destination:
                                TimerView(...),
                               tag: .timer, selection: $programmaticNavigationDestination)
                        .frame(width: 0, height: 0)

在我的 TimerView 的初始化中,通知终于被注册了

        self.endDate = Date().fromTimeMillis(timeMillis: timerServiceRelevantVars.endOfCountDownInMilliseconds_date)
        
        // set a countdown Finished notification to the end of countdown
        let calendar = Calendar.current
        let notificationComponents = calendar.dateComponents([.hour, .minute, .second], from: endDate)
        let trigger = UNCalendarNotificationTrigger(dateMatching: notificationComponents, repeats: false)
        
        
        let content = UNMutableNotificationContent()
        content.title = "Countdown Finished"
        content.subtitle = "the countdown finished"
        content.sound = UNNotificationSound.defaultCritical

        // choose a random identifier
        let request2 = UNNotificationRequest(identifier: "endCountdown", content: content, trigger: trigger)

        // add the notification request
        UNUserNotificationCenter.current().add(request2)
        {
            (error) in
            if let error = error
            {
                print("Uh oh! We had an error: \(error)")
            }
        }

如上所述,当用户在除我自己的应用之外的任何地方时,通知都会按预期显示。然而,TimerView 显示有关倒计时的信息,并且最好是用户设备上的活动视图。因此,我需要能够在此处接收通知,但也需要在我的应用程序中的其他任何地方接收通知,因为用户还可以在我的应用程序中的其他地方导航。如何实现?

this example中完成了类似的事情,不幸的是不是用swiftUI写的,而是用以前的通用语言写的。我不明白这是如何完成的,或者如何完成的。我在互联网上没有找到任何关于这个的东西。希望你能帮助我。

参考文档:

Scheduling and Handling Local Notifications
关于当您的应用程序位于前台时处理通知的部分:

If a notification arrives while your app is in the foreground, you can silence that notification or tell the system to continue to display the notification interface. The system silences notifications for foreground apps by default, delivering the notification’s data directly to your app...

据此,您必须为 UNUserNotificationCenter 实现委托并调用 completionHandler 来告知您希望如何处理通知。 我建议你这样做,在 AppDelegate 上你为 UNUserNotificationCenter 分配委托,因为文档说它必须在应用程序完成启动之前完成(请注意文档说委托应该在应用程序完成启动之前设置):

// AppDelegate.swift
class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        return true
    }
}

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        // Here we actually handle the notification
        print("Notification received with identifier \(notification.request.identifier)")
        // So we call the completionHandler telling that the notification should display a banner and play the notification sound - this will happen while the app is in foreground
        completionHandler([.banner, .sound])
    }
}

并且您可以通过在 App 场景中使用 UIApplicationDelegateAdaptor 告诉 SwiftUI 使用此 AppDelegate

@main
struct YourApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

此方法类似于 Apple 的 Fruta:使用 SwiftUI 构建功能丰富的应用程序

https://developer.apple.com/documentation/swiftui/fruta_building_a_feature-rich_app_with_swiftui

Apple 以这种方式使用应用内购买


此 class 包含您与通知相关的所有代码。

class LocalNotificaitonCenter: NSObject, ObservableObject {
    //  .....
}

在您的 @main App 结构中,将 LocalNotificaitonCenter 定义为 @StateObject 并将其作为 environmentObject 传递给子视图

@main
struct YourApp: App {
    @Environment(\.scenePhase) private var scenePhase
    
    @StateObject var localNotificaitonCenter = LocalNotificaitonCenter()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(localNotificaitonCenter)
        }
    }
}

就是这样!