4

I believe this has been asked several times, but there is no clear answer.

There are two ways to schedule temporal notifications: UNCalendarNotification and UNTimeIntervalNotificationTrigger.

Scheduling a notification for a specific time and day of the week is trivial, same with on a day specific of the month, but scheduling for a specific time, every n days is not so trivial.

E.g. Every 5 days at 11:00.

UNTimeIntervalNotificationTrigger may seem like the right class to reach for, except problems are introduced when daylight savings or timezone changes occur. E.g. Summer time ends and now your notification is at 10:00 instead of 11:00.

The day property on the DateComponents class together with the UNCalendarNotification class may hold the solution because it says in the docs "A day or count of days". I interpret this as "A specific day of the month (A day) or n number of days (count of days)".

Digging further into the day property docs, it reads "This value is interpreted in the context of the calendar in which it is used".

How can you use the day property together with the context of the calendar to work with a count of days instead of specific days of the month?

Additionally, the docs for the hour and minute properties in DateComponents also read "An hour or count of hours" and "A minute or count of minutes" respectively. So even if you were to set day to "a count of days" how might you set the hour and minute correctly?

It's clear this functionality is possible in iOS - the Reminders app is evidence of this.

Marc Greenstock
  • 11,278
  • 4
  • 30
  • 54
  • Every n days based on what date? And there is no difference as long as you stick to the same time zone or just not set a time difference at all. – El Tomato May 25 '21 at 07:03
  • @ElTomato Thank you for your comment, yes "every n days" needs an anchor date, but how do you set the anchor date? I understand the `calendar` property can be set on `DateComponents`, but does this change the context when setting the `day` property from "a day" to "a count of days"? – Marc Greenstock May 25 '21 at 07:29
  • See `date(byAdding:to:options:)`. You can use `.day`. – El Tomato May 25 '21 at 07:35

1 Answers1

5

You can set them up upfront using UNCalendarNotificationTrigger for an n number of times and using an adjusted calendar for the current timeZone

import SwiftUI

class NotificationManager: NSObject, UNUserNotificationCenterDelegate{
    static let shared: NotificationManager = NotificationManager()
    let notificationCenter = UNUserNotificationCenter.current()
    
    private override init(){
        super.init()
        requestNotification()
        notificationCenter.delegate = self
        getnotifications()
    }
    
    func requestNotification() {
        print(#function)
        notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
            
            if let error = error {
                // Handle the error here.
                print(error)
            }
            
            // Enable or disable features based on the authorization.
        }
    }
    /// Uses [.day, .hour, .minute, .second] in current timeZone
    func scheduleCalendarNotification(title: String, body: String, date: Date, repeats: Bool = false, identifier: String) {
        print(#function)
        
        let content = UNMutableNotificationContent()
        content.title = title
        content.body = body
        
        let calendar = NSCalendar.current

        let components = calendar.dateComponents([.day, .hour, .minute, .second], from: date)
        
        let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: repeats)
        
        let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
        notificationCenter.add(request) { (error) in
            if error != nil {
                print(error!)
            }
        }
    }
    ///Sets up multiple calendar notification based on a date
    func recurringNotification(title: String, body: String, date: Date, identifier: String, everyXDays: Int, count: Int){
        print(#function)
        for n in 0..<count{
            print(n)
            let newDate = date.addingTimeInterval(TimeInterval(60*60*24*everyXDays*n))
            //Idenfier must be unique so I added the n
            scheduleCalendarNotification(title: title, body: body, date: newDate, identifier: identifier + n.description)
            print(newDate)
        }
    }
    ///Prints to console schduled notifications
    func getnotifications(){
        notificationCenter.getPendingNotificationRequests { request in
            for req in request{
                if req.trigger is UNCalendarNotificationTrigger{
                    print((req.trigger as! UNCalendarNotificationTrigger).nextTriggerDate()?.description ?? "invalid next trigger date")
                }
            }
        }
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        
        completionHandler(.banner)
    }
}
class ZuluNotTriggerViewModel:NSObject, ObservableObject, UNUserNotificationCenterDelegate{
    @Published var currentTime: Date = Date()
    let notificationMgr = NotificationManager.shared
    
    
    ///Sets up multiple calendar notification based on a date
    func recurringNotification(title: String, body: String, date: Date, identifier: String, everyXDays: Int, count: Int){
        print(#function)
        notificationMgr.recurringNotification(title: title, body: body, date: date, identifier: identifier, everyXDays: everyXDays, count: count)
        
        //just for show now so you can see the current date in ui
        self.currentTime = Date()
    }
    ///Prints to console schduled notifications
    func getnotifications(){
        notificationMgr.getnotifications()
    }
    
}
struct ZuluNotTriggerView: View {
    @StateObject var vm: ZuluNotTriggerViewModel = ZuluNotTriggerViewModel()
    var body: some View {
        VStack{
            Button(vm.currentTime.description, action: {
                vm.currentTime = Date()
            })
            Button("schedule-notification", action: {
                let twoMinOffset = 120
                //first one will be in 120 seconds
                //gives time to change settings in simulator
                //initial day, hour, minute, second
                let initialDate = Date().addingTimeInterval(TimeInterval(twoMinOffset))
                //relevant components will be day, hour minutes, seconds
                vm.recurringNotification(title: "test", body: "repeat body", date: initialDate, identifier: "test", everyXDays: 2, count: 10)
            })
            
            Button("see notification", action: {
                vm.getnotifications()
            })
        }
    }
}

struct ZuluNotTriggerView_Previews: PreviewProvider {
    static var previews: some View {
        ZuluNotTriggerView()
    }
}
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48
  • Hi Lorem ipsum, thanks for such a detailed answer. The original bounty on the answer expired before I had a chance to reward you, I've started a new one and intend on rewarding it once the button is available. – Marc Greenstock Jun 30 '21 at 11:47
  • Hi @lorem ipsum, what if we don't know the count? For example we want to set reminder for every 2 days but without expiration date. Share with us if you have any idea, thanks in advice – Coder ACJHP Aug 03 '22 at 11:51
  • 1
    @CoderACJHP there is a limit to how many notifications you can put in the queue. I don’t remember the number. I personally wouldn’t put more than a month at a time. Use the notifications that apple has to replenish the queue every time the app comes into the foreground. If the user hasn’t tapped a notification or opened your app in a month notifications likely become an annoyance. If you are using a server you can also set notifications from the server it is much more dynamic that way. – lorem ipsum Aug 03 '22 at 12:04