Schedule local notification every n days (timezone safe) - ios

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.

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

Related

Execute a func when the device time change

I'm trying to execute a func when the device clock change.
This func will return the next course of a student, all the courses are in a array.
if I understand correctly we can not execute a func when the clock of the device change.
I read some topics where people say to do a timer of 60s or other but if the user launch the app a 08:05:07 the func will execute with 7s of late.
I thought to use a do while but I think it will use the CPU a lot and so the battery too. no ?
Does anyone have an idea ?
If you’re just saying that you want to fire a timer at some specific future Date, you should just calculate the amount of time between now and then (using timeIntervalSince), and then use that.
For example, it’s currently "2019-01-20 17:11:59 +0000”, but if I want it to fire at 17:15, you can do:
weak var timer: Timer?
func startTimer() {
let futureDate = ISO8601DateFormatter().date(from: "2019-01-20T17:15:00Z")!
let elapsed = futureDate.timeIntervalSince(Date()) // will be roughly 180.56 in this example at this moment of time
timer?.invalidate() // invalidate prior timer, if any
timer = Timer.scheduledTimer(withTimeInterval: elapsed, repeats: false) { [weak self] _ in
// whatever you want to do at 17:15
}
}
Clearly, however you come up with futureDate will be different in your case, but it illustrates the idea. Just calculate the elapsed time between the future target date and now, and use that for the timer.
Now, if you’re really worried about changes to clock, in iOS you might observe significantTimeChangeNotification, e.g.,
NotificationCenter.default.addObserver(forName: UIApplication.significantTimeChangeNotification, object: nil, queue: .main) { [weak self] _ in
// do something, maybe `invalidate` existing timer and create new one
self?.startTimer()
}
I thought to use a do while but I think it will use the CPU a lot and so the battery too. no ?
Yes, spinning in a loop, waiting for some time to elapse, is always a bad idea. Generally you’d just set a timer.
This func will return the next course of a student, all the courses are in a array.
This begs the question whether the app will be running in the foreground or not. If you want to notify the user at some future time, whether they’re running the app right now or not, consider "user notifications”. E.g. request permission for notification:
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
if !granted {
// warn the user that they won't be notified after the user leaves the app unless they grant permission for notifications
DispatchQueue.main.async {
let alert = UIAlertController(title: nil, message: "We need permission to notify you of your class", preferredStyle: .alert)
if let url = URL(string: UIApplication.openSettingsURLString) {
alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
UIApplication.shared.open(url)
})
}
alert.addAction(UIAlertAction(title: "Cancel", style: .default))
self.present(alert, animated: true)
}
}
}
And then, assuming that permissions have been granted, then schedule a notification:
let content = UNMutableNotificationContent()
content.title = "ClassTime"
content.body = "Time to go to math class"
let components = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: futureDate)
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
let request = UNNotificationRequest(identifier: "Math 101", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
I did this
private var secondesActuelles : Int = 0
private var timer = Timer()
private func récuperLesSecondes(date : Date) -> Int {
let date = date
let calendar = Calendar.current
let second = calendar.component(.second, from: date)
return second
}
override func viewDidLoad() {
super.viewDidLoad()
secondesActuelles = récuperLesSecondes(date: Date())
timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(60 - secondesActuelles), repeats: false, block: { (timer) in
print("l'heure a changé")
self.secondesActuelles = self.récuperLesSecondes(date: Date())
print("secondesActuelles : \(self.secondesActuelles)")
})
}
It work once. but after the timeInterval don't change.

Triggering all pending notifications before the time set for it

In the app I'm developing, there is an option for triggering notification x amount of time before the actual time set for the said notification. For example I set the reminder for 10:00. But in the app's local settings, I set the notification to trigger 10 minutes before the time set. So, in this example's case, the notification will trigger in 9:50.
Now, I can do the above when I'm setting time for individual notification. But what I want to do is trigger all pending notifications before the actual time set for it.
This is the function I'm using to set notifications:
func scheduleNotification(at date: Date, identifier: String, threadIdentifier: String, body: String) {
let calendar = Calendar(identifier: .gregorian)
let components = calendar.dateComponents(in: .current, from: date)
let newComponents = DateComponents(calendar: calendar, timeZone: .current, year: components.year, month: components.month, day: components.day, hour: components.hour, minute: components.minute)
let trigger = UNCalendarNotificationTrigger(dateMatching: newComponents, repeats: false)
let content = UNMutableNotificationContent()
content.title = "TestNotification"
content.body = body
content.threadIdentifier = threadIdentifier
content.sound = UNNotificationSound.default()
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) {
error in
if let error = error {
print("Error in delivering notification. Error: \(error.localizedDescription)")
}
}
}
The date is coming from the date set by the date picker. I tried to change trigger properties by using this code:
UNUserNotificationCenter.current().getPendingNotificationRequests { (requests) in
for request in requests {
request.trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10*60, repeats: false)
}
}
But now I get an error saying 'trigger' is a get-only property.
There is no way to change fire time of a scheduled notification , you can remove all of them and re-schedule again

Swift 3 Local notifications not firing

I have the following setup and no notification are firing at all.
Based on other similar questions on the stack, I've put in a unique identifier for each request and I've also added the body to the content.
I've got this function which requests permission from user.
func sendIntNotifications()
{
//1. Request permission from the user
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound], completionHandler:
{
(granted, error) in
if granted
{
print ("Notification access granted")
}
else
{
print(error?.localizedDescription)
}
UNUserNotificationCenter.current().delegate = self
})
// This function has a lot of other code that then calls this function to set multiple notifications :
for daysInt in outputWeekdays
{
scheduleActualNotification(hourInt: hourInt, minuteInt: minuteInt, dayInt: daysInt)
}
}
And these are the main function :
func scheduleActualNotification(hourInt hour: Int, minuteInt minute: Int, dayInt day: Int)
{
// 3.1 create date components for each day
var dateComponents = DateComponents()
dateComponents.hour = hour
dateComponents.minute = minute
dateComponents.day = day
//3.2 Create a calendar trigger for that day at that time
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents,
repeats: true)
//3.3 Message content
let content = UNMutableNotificationContent()
content.title = "Test Title"
content.body = "Test body"
content.badge = 1
// 3.4 Create the actual notification
let request = UNNotificationRequest(identifier: "bob \(i+1)",
content: content,
trigger: trigger)
// 3.5 Add our notification to the notification center
UNUserNotificationCenter.current().add(request)
{
(error) in
if let error = error
{
print("Uh oh! We had an error: \(error)")
}
}
}
This function is to receive the notification when this app is open
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void)
{
completionHandler([.alert, .sound]) //calls the completionHandler indicating that both the alert and the sound is to be presented to the user
}
There are no errors either lol!
Edit :
So from the user interface, I selected 2350 and Saturday and Sunday. I expect the notification to be sent at 2350 on both Saturday and Sunday.
I did print(daysInt). The first value of it is 1 and the second value of it is 7 which is just as expected. Secondly, I went into the function scheduleActualNotification and the values of hour was 23 and minute was 50, just as expected.
Thank you!
So after weeks of frustration, this is the solution :
Replace dateComponents.day = day with dateComponents.weekday = day. .day is for day of the month whereas .weekday is day of the week!
Also do not use dateComponents.year = 2017 as that caused the notifications not to fire.
The notifications work perfectly fine now!!
Calendar Unit Flags can help you :
var notificationTriggerCalendar : UNCalendarNotificationTrigger? = nil
if let fireDate = trigger.remindAt {
let unitFlags = Set<Calendar.Component>([.hour, .year, .minute, .day, .month, .second])
var calendar = Calendar.current
calendar.timeZone = .current
let components = calendar.dateComponents(unitFlags, from: fireDate)
let newDate = Calendar.current.date(from: components)
notificationTriggerCalendar = UNCalendarNotificationTrigger.init(dateMatching: components, repeats: true)
}

UNCalendarNotificationTrigger doesn't get stored unless repeats is true

I've noticed that if I create an UNCalendarNotificationTrigger with a custom date it does't get added unless i put:
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: **true**)
Apple Example is:
let date = DateComponents()
date.hour = 8
date.minute = 30
let trigger = UNCalendarNotificationTrigger(dateMatching: date, repeats: true)
which make sense to be repeats == true.
In my scenario I dont need to create one notification that gets repeated many times, but I need multiple notificaitons fired only once on a specific calendar date (which is of course in the future)..
If I'm doing:
let calendar = Calendar(identifier: .gregorian)
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMdd"
let newdate = formatter.date(from: "20161201")
let components = calendar.dateComponents(in: .current, from: newdate!)
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
then i always get 0 pending notifications...
UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: { (notifications) in
print("num of pending notifications \(notifications.count)")
})
num of pending notification 0
Any idea?
EDIT1:
Adding other context as pointed out by one of the answers.
I'm actually adding the request to the current UNUserNotificationQueue.
let request = UNNotificationRequest(identifier: "future_calendar_event_\(date_yyyyMMdd)", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
// Do something with error
print(error.localizedDescription)
} else {
print("adding \((request.trigger as! UNCalendarNotificationTrigger).dateComponents.date)")
}
}
I have the same problem and I solve it now. It is due to dateComponents' year.
In order to solve this problem, I test the following code:
1.
let notificationCenter = UNUserNotificationCenter.current()
let notificationDate = Date().addingTimeInterval(TimeInterval(10))
let component = calendar.dateComponents([.year,.day,.month,.hour,.minute,.second], from: notificationDate)
print(component)
let trigger = UNCalendarNotificationTrigger(dateMatching: component, repeats: false)
let request = UNNotificationRequest(identifier: item.addingDate.description, content: content, trigger: trigger)
self.notificationCenter.add(request){(error) in
if let _ = error {
assertionFailure()
}
}
In console, print component:
year: 106 month: 2 day: 14 hour: 12 minute: 3 second: 42 isLeapMonth: false
And in this case, the notification cannot be found in pending notification list.
2.When I set component's year explicitly to 2017:
let notificationDate = Date().addingTimeInterval(TimeInterval(10))
var component = calendar.dateComponents([.year,.day,.month,.hour,.minute,.second], from: notificationDate)
component.year = 2017
print(component)
let trigger = UNCalendarNotificationTrigger(dateMatching: component, repeats: false)
let request = UNNotificationRequest(identifier: item.addingDate.description, content: content, trigger: trigger)
self.notificationCenter.add(request){(error) in
if let _ = error {
assertionFailure()
}
}
In console, the component is:
year: 2017 month: 2 day: 14 hour: 12 minute: 3 second: 42 isLeapMonth: false
Then this notification can be found in pending notification list.
And next, I check in the pending notification requests to find whether the trigger-date's year component is 106 or 2017:
notificationCenter.getPendingNotificationRequests(){[unowned self] requests in
for request in requests {
guard let trigger = request.trigger as? UNCalendarNotificationTrigger else {return}
print(self.calendar.dateComponents([.year,.day,.month,.hour,.minute,.second], from: trigger.nextTriggerDate()!))
}
}
I find the trigger's nextTriggerDate components are:
year: 106 month: 2 day: 14 hour: 12 minute: 3 second: 42 isLeapMonth: false
Conclusion
So if you want to set the trigger's repeats to false, you should make sure the trigger date is bigger than current date.
The default dateComponents' year may be unsuitable, such as 106. If you want the notification to fire in 2017, you should set the components year to 2017 explicitly.
Perhaps this is a bug, because I set trigger's dateComponents' year to 2017 but get 106 in nextTriggerDate of pending notification request.
On my app, I request permission after notification set by mistake. So If I want to get pending notification count, I got 0.
I requested permission on AppDelegate but notifications setted on first view viewdidload(). I added a notification function to trigger with button. After get permission click button and finally setted my notification and I got pending notification count 1.
I hope that's will help you.
UNCalendarNotificationTrigger creates the schedule for which a notification should occur, but it does not do the scheduling. For this you need to create a UNNotificationRequest and then add this to the notification center. Something along the lines of:
let request = UNNotificationRequest(identifier: "MyTrigger", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
// Do something with error
} else {
// Request was added successfully
}
}

Run a function every day or every hour in Swift

I can do it manually with the following code:
var myDate:NSDateComponents = NSDateComponents()
myDate.year = 2015
myDate.month = 04
myDate.day = 20
myDate.hour = 12
myDate.minute = 38
myDate.timeZone = NSTimeZone.systemTimeZone()
var calendar:NSCalendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
var date:NSDate = calendar.dateFromComponents(myDate)!
var notification:UILocalNotification = UILocalNotification()
notification.category = "First Category"
notification.alertBody = "Hi, I'm a notification"
notification.fireDate = date
UIApplication.sharedApplication().scheduleLocalNotification(notification)
But how can I run it every hour or every day? Any idea?
First: add an extension to the Date class:
extension Date {
func currentTimeMillis() -> Int64 {
return Int64(self.timeIntervalSince1970 * 1000)
}
}
then call this function in the viewDidLoad():
func run24HoursTimer() {
let currentDate = Date()
let waitingDateTimeInterval:Int64 = UserDefaults.standard.value(forKey: "waiting_date") as? Int64 ?? 0
let currentDateTimeInterval = currentDate.currentTimeMillis()
let dateDiffrence = currentDateTimeInterval - waitingDateTimeInterval
if dateDiffrence > 24*60*60*1000 {
// Call the function that you want to be repeated every 24 hours here:
UserDefaults.standard.setValue(currentDateTimeInterval, forKey: "waiting_date")
UserDefaults.standard.synchronize()
}
}
There is a separate property on a local notification called repeatInterval. See reference
notification.repeatInterval = .Day
Also keep in mind to register in application delegate (didFinishLaunchingWithOptions method) for local notification (alert asking for permission will be presented for the first time). In Swift this will be (an example):
if UIApplication.instancesRespondToSelector(Selector("registerUserNotificationSettings:"))
{
application.registerUserNotificationSettings(UIUserNotificationSettings(forTypes: [.Sound, .Alert, .Badge], categories: nil))
}
I would also recommend setting time zone for the notification, could be like this (example):
notification.timeZone = NSTimeZone.localTimeZone()
Not sure about "run function every...". This will create a notification fired with the specified repeat interval. I found this tutorial helpful.
Use This :-
1). save your daily time in user defaults
2). set notification on time for next day
3). check in app delegate if time is passed or not
4). if it is passed then set next day notification
5). if you change time update user defaults
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
let request = UNNotificationRequest(identifier: indentifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: {
(errorObject) in
if let error = errorObject{
print("Error \(error.localizedDescription) in notification \(indentifier)")
}
})
You mean something like this?
let timer = NSTimer(timeInterval: 3600, target: self, selector: "test", userInfo: nil, repeats: false)
func test() {
// your code here will run every hour
}
Put all that code in one class. Much more info at #selector() in Swift?
//Swift >=3 selector syntax
let timer = Timer.scheduledTimer(timeInterval: 3600, target: self, selector: #selector(self.test), userInfo: nil, repeats: true)
func test() {
// your code here will run every hour
}
Note: Time Interval is in seconds
Reference : How can I use NSTimer in Swift?

Resources