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