3

Okay - I am totally frustrated with this piece of code right now and ready to give up! Basically when simulating to either Simulator or actual device I get the requestAuthorisation to work no problem but the trigger does not initiate ever. I have followed several guys online and their code worked with ease! When I use a button to initiate a UNTimeIntervalNotificationTrigger it works but that is not what I want. Currently testing in iOS 14.3 as target for build. Rest of the App builds no problem. What am I doing wrong?! Cannot help but think that somewhere along the line of trying to get it to work I might have damaged something in info.plist or similar?! I have tested to repeat the trigger and not to repeat but neither works.

    override func viewDidLoad() {
        super.viewDidLoad()
        
        //NOTIFICATIONS
        // Step 1 - Ask the use for permission to notify
        let randVerseCenter = UNUserNotificationCenter.current()
        randVerseCenter.requestAuthorization(options: [.alert, .sound]){ (granted, error) in
            if granted {
                print("Yay - request authorisation worked!")
            } else {
                print ("D'oH - request Authorisation did not work!")
            }
        }
        // Step 2 - Create the Notification Content
        let randVerseContent = UNMutableNotificationContent()
        randVerseContent.title = "Random Reference"
        randVerseContent.body = "Random Verse"
        randVerseContent.sound = UNNotificationSound.default
        // Step 3 - Create the trigger for the notification by delay
        let randVerseDate = Date().addingTimeInterval(30)
        let randVerseDateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: randVerseDate)
        let randVerseTrigger = UNCalendarNotificationTrigger(dateMatching: randVerseDateComponents, repeats: true)
        // Step 4 - Creating the request
        let randVerseUUIDString = UUID().uuidString
        let randVerseRequest = UNNotificationRequest(identifier: randVerseUUIDString, content: randVerseContent, trigger: randVerseTrigger)
        // Step 5 - Register the request
        randVerseCenter.add(randVerseRequest) { (error) in
            if let error = error{
                print (error.localizedDescription)
            }
            //Check the error parameter and handle any errors
        }
    }
Jobert
  • 1,532
  • 1
  • 13
  • 37
ST101
  • 55
  • 1
  • 6

2 Answers2

3

After getting more details, I guess I know why you still don't see the notifications being delivered. I'm making it in another answer to not have it too long, but I'll keep my previous answer for reference.
Maybe you were waiting for the notification with the application in foreground? I'll refer to another part of 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...

So, if that's the case, you must implement a delegate for UNUserNotificationCenter.
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:

// AppDelegate.swift
@main
class AppDelegate: UIResponder, UIApplicationDelegate {


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

    // Rest of your code on AppDelegate...
}

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

On the view controller you have handling the notification authorization and request registration, you could do it like this:

class NotificationsViewController: UIViewController {
    
    static let notificationAuthorizedNotification = NSNotification.Name(rawValue: "NotificationAuthorizedNotification")
    let randVerseCenter = UNUserNotificationCenter.current()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // We call this method when we know that the user granted permission, so we know we can then make notification requests
        NotificationCenter.default.addObserver(self, selector: #selector(handleNotificationAuthorization), name: NotificationsViewController.notificationAuthorizedNotification, object: nil)
        
        randVerseCenter.getNotificationSettings { [weak self] settings in
            // We check current settings and asks for permission if not granted before
            if settings.authorizationStatus == .notDetermined {
                // Step 1 - Ask the use for permission to notify
                self?.randVerseCenter.requestAuthorization(options: [.alert, .sound]){ (granted, error) in
                    if granted {
                        NotificationCenter.default.post(name: NotificationsViewController.notificationAuthorizedNotification, object: nil)
                        print("Yay - request authorisation worked!")
                    } else {
                        print ("D'oH - request Authorisation did not work!")
                    }
                }
            }
        }
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        // We stop listening to those notifications here
        NotificationCenter.default.removeObserver(self)
    }
    
    @objc
    func handleNotificationAuthorization() {
        // Step 2 - Create the Notification Content
        let randVerseContent = UNMutableNotificationContent()
        randVerseContent.title = "Random Reference"
        randVerseContent.body = "Random Verse"
        randVerseContent.sound = UNNotificationSound.default
        // Step 3 - Create the trigger for the notification by delay
        let randVerseDate = Date().addingTimeInterval(30)
        let randVerseDateComponents = Calendar.current.dateComponents([.second], from: randVerseDate)
        let randVerseTrigger = UNCalendarNotificationTrigger(dateMatching: randVerseDateComponents, repeats: true)
        // Step 4 - Creating the request
        let randVerseUUIDString = UUID().uuidString
        let randVerseRequest = UNNotificationRequest(identifier: randVerseUUIDString, content: randVerseContent, trigger: randVerseTrigger)
        // Step 5 - Register the request
        randVerseCenter.add(randVerseRequest) { (error) in
            if let error = error{
                print (error.localizedDescription)
            } else {
                print("Successfully registered notification with id \(randVerseUUIDString) at every second \(randVerseDateComponents.second!) of a minute")
            }
        }
    }
}

You might still have older notifications scheduled since your code was requesting them at the viewDidLoad and maybe you didn't remove them or delete the app.
You can check the pending notifications using this on your viewDidLoad for example:

        randVerseCenter.getPendingNotificationRequests() { requests in
            for request in requests {
                guard let trigger = request.trigger as? UNCalendarNotificationTrigger else { return }
                print("Notification registered with id \(request.identifier) is schedulled for \(trigger.nextTriggerDate()?.description ?? "(not schedulled)")")
            }
        }

And use randVerseCenter to remove them by their identifiers or remove all of them.

Jobert
  • 1,532
  • 1
  • 13
  • 37
2

The problem is how the trigger was created. We can look at the documentation for UNCalendarNotificationTrigger to get more understanding:

Create a UNCalendarNotificationTrigger object when you want to schedule the delivery of a local notification at the specified date and time. You specify the temporal information using an NSDateComponents object, which lets you specify only the time values that matter to you. The system uses the provided information to determine the next date and time that matches the specified information.

https://developer.apple.com/documentation/usernotifications/uncalendarnotificationtrigger

So, you use UNCalendarNotificationTrigger when you want to create a trigger to match the date components. The code below will create a trigger which will deliver a notification every day at 8:30 in the morning, because the .hour and the .minute components were specified:

    var date = DateComponents()
    date.hour = 8
    date.minute = 30 
    // This trigger will match these two components - hour and minute
    let trigger = UNCalendarNotificationTrigger(dateMatching: date, repeats: true)

In your case, you created a trigger using all of the components of a date (year, month, dat, hour, minute, second):

let randVerseDateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: randVerseDate)

And that makes it an impossible condition to repeat the trigger - because there won't be another year 2021 - so it will not be triggered.

You need to think how you want this notification to be triggered. If your intention is to deliver a notification on the same second counting from a specific time, then you must use only the .second date component:

let randVerseDateComponents = Calendar.current.dateComponents([.second], from: randVerseDate)

Let's say randVerseDate is something like 2021-01-06-20:01:35, and we use the line of code above. Then this will trigger the notification every minute when the clock reaches 35 seconds: 20:02:35, then 20:03:35, then 20:04:35, and so on...

Jobert
  • 1,532
  • 1
  • 13
  • 37
  • Many many thanks for your help @Jobert. I tried what you had stated to just remove all of the components from the dateComponents array except .second. However, I still have not success. I had also tried at an earlier stage to implement the code that you had mentioned earlier - i.e. an exact time where the notification would repeat every day but this also does not work. This method would also meet my goals but I could not get it to work. I read in a forum that a repeats: true of less than 1 minute would not work but even anything >61 secs will not work for me. Thanks for your help anyway. – ST101 Jan 06 '21 at 21:46
  • I keep thinking the code is fine because when I run it the Registering of the request works (added a bit of code to return the uuidString) just to check. I think I have definitely done something in my Xcode software somewhere. If I run the code even in a completely blank project in the viewDidLoad with nothing else it does not work for me. Authorisation works fine but the notification never comes through @Jobert. Thanks again! – ST101 Jan 06 '21 at 21:54