每 n 天安排一次本地通知(时区安全)

人气:893 发布:2022-10-16 标签: ios swift uilocalnotification nsdatecomponents unusernotificationcenter

问题描述

我相信这已经被问过好几次了,但没有明确的答案.

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

有两种安排时间通知的方法:UNCalendarNotificationUNTimeIntervalNotificationTrigger.

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

为特定时间和一周中的某天安排通知是微不足道的,与每月的特定日期相同,但针对特定时间(每 n 天)安排通知并不是那么简单.

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.

例如每 5 天 11:00.

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

UNTimeIntervalNotificationTrigger 似乎是合适的类,除非在发生夏令时或时区更改时引入问题.例如.夏令时结束,现在您的通知时间为 10:00,而不是 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.

DateComponents 类和 UNCalendarNotification 类可能包含解决方案,因为它在文档中说一天"或天数"".我将其解释为一个月中的特定日期(一天)或 n 天数(天数)".

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

深入了解 day 属性文档,它读作这个值是在使用它的日历的上下文中解释的".

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

如何将 day 属性与日历的上下文一起使用来处理天数而不是一个月中的特定天数?

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?

此外,DateComponentshourminute 属性的文档也显示为一小时或小时数".和一分钟或分钟数"分别.因此,即使您将 day 设置为天数"你如何正确设置hourminute?

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?

很明显,此功能在 iOS 中是可行的 - 提醒应用就是证明.

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

推荐答案

您可以使用 UNCalendarNotificationTrigger 预先设置它们 n 次并使用调整后的日历当前时区

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.addingTimeInterval(TimeInterval(TimeZone.current.secondsFromGMT())))
        
        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()
    }
}

823