iosswiftnotificationsswiftuicountdown

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


I am redeveloping an android app for iOS with SwiftUI that contains a countdown feature. When the countdown finishes the user should be noticed about the end of the countdown. The Notification should be somewhat intrusive and work in different scenarios e.g. when the user is not actively using the phone, when the user is using my app and when the user is using another app. I decided to realize this using Local Notifications, which is the working approach for android. (If this approach is totally wrong, please tell me and what would be best practice)

However I am stuck receiving the notification when the user IS CURRENTLY using my app. The Notification is only being shown in message center (where all notifications queue) , but not actively popping up.

Heres my code so far: The User is being asked for permission to use notifications in my CountdownOrTimerSheet struct (that is being called from a different View as 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)
        }
    }
}

Only if the user allows permission for notification my TimerView struct is being called

                         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
                            }
                        }

from the previous View

                   .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)

In my TimerView's init the notification is finally registered

        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)")
            }
        }

As mentioned above the notification gets shown as expected when the user is everyWhere but my own app. TimerView however displays information about the countdown and is preferably the active view on the users device. Therefore I need to be able to receive the notification here, but also everywhere else in my app, because the user could also navigate somewhere else within my app. How can this be accomplished?

In this example a similar thing has been accomplished, unfortunately not written in swiftUI but in the previous common language. I do not understand how this was accomplished, or how to accomplish this.. I did not find anything on this on the internet.. I hope you can help me out.


Solution

  • With reference to the documentation:

    Scheduling and Handling Local Notifications
    On the section about Handling Notifications When Your App Is in the Foreground:

    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...

    Acording to that, you must implement a delegate for UNUserNotificationCenter and call the completionHandler telling how you want the notification to be handled. I suggest you something like this, where on AppDelegate you assign the delegate for UNUserNotificationCenter since documentation says it must be done before application finishes launching (please note documentation says the delegate should be set before the app finishes launching):

    // 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])
        }
    }
    

    And you can tell SwiftUI to use this AppDelegate by using the UIApplicationDelegateAdaptor on your App scene:

    @main
    struct YourApp: App {
        @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }