Repeating local notification after a given date - swift

I need to create different types of notifications. I managed to create a notification at a specific date, but now I need to create one that repeats daily, after a start date. It's basically a reminder to take a daily medication, but the user will only start taking that medication at a specific day.
Here is my func:
func addNotification(text: String, date: Date, id: String) {
let content = UNMutableNotificationContent()
content.title = "My App"
content.body = text
content.sound = .default
content.badge = NSNumber(integerLiteral: UIApplication.shared.applicationIconBadgeNumber + 1)
var dateComponents = DateComponents()
dateComponents.hour = Calendar.current.component(.hour, from: date)
dateComponents.minute = Calendar.current.component(.minute, from: date)
var trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
let request = UNNotificationRequest(identifier: id, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error {
print(error.localizedDescription)
}
}
}
As it is, it will notify daily, but from the moment the func is fired. I need to receive a start date and only begin firing after that.
One thing that comes to mind is: if I also specify the year, month and day on "dayComponents" like this:
dateComponents.hour = Calendar.current.component(.hour, from: date)
dateComponents.minute = Calendar.current.component(.minute, from: date)
dateComponents.day = Calendar.current.component(.day, from: date)
dateComponents.month = Calendar.current.component(.month, from: date)
dateComponents.year = Calendar.current.component(.year, from: date)
and set the trigger to repeat, would it consider all the parameters (hour, minute, day, month, year) and never repeat (as this day will never happen again), or will it only consider hour and minute to repeat? I wanted to try that, but then I'd need to wait for a whole day, so maybe someone here knows the answer!

Related

How do I schedule a notification at a specific time, and then repeat it every x amount of time?

I am making a reminder app where you can schedule a reminder, that will then repeat every x seconds/minutes/hours/days etc.
If I want it to repeat every x amount of time I can do it like so:
func addNotification() {
let content = UNMutableNotificationContent()
content.title = "title"
// show this notification 5 minutes from now
var trigger: UNTimeIntervalNotificationTrigger
trigger = UNTimeIntervalNotificationTrigger(timeInterval: 300, repeats: true)
// choose a random identifier
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
// add our notification request
UNUserNotificationCenter.current().add(request)
}
This is essentially what I want, but instead of starting 5 minutes from now, I want to be able to choose the start date and then have it repeat every 5 minutes seconds from that initial start date.
Is this possible?
From what I know it's not possible to repeat a notification every X seconds (or something else) after a specific date.
I think the "best" option here is to use UNCalendarNotificationTrigger instead and schedule 60/5 = 12 notification (so 1 every 5 seconds) starting from the given date.
Something like this:
// this is your reference date - here it's now + 5 seconds just for this example
var referenceDate = Calendar.current.date(byAdding: .second, value: 5, to: Date())!
for i in 0...11 { // one every 5 seconds, so total = 12
let content = UNMutableNotificationContent()
content.title = "Notif \(i)"
content.body = "Body"
var dateComponents = DateComponents(calendar: Calendar.current)
// 5 seconds interval here but you can set whatever you want, for hours, minutes, etc.
dateComponents.second = 5
//dateComponents.hour = X
// [...]
guard let nextTriggerDate = dateComponents.calendar?.date(byAdding: dateComponents, to: referenceDate),
let nextTriggerDateCompnents = dateComponents.calendar?.dateComponents([.second], from: nextTriggerDate) else {
return
}
referenceDate = nextTriggerDate
print(nextTriggerDate)
let trigger = UNCalendarNotificationTrigger(dateMatching: nextTriggerDateCompnents, repeats: true)
let request = UNNotificationRequest(identifier: "notif-\(i)", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
}
Now based on that, you would need to handle when a user taps one of the notifications in order to cancel all the others. But that's another topic here and I leave it up to you to find the logic for that.

Error Creating Simple Countdown to Date App in Swift 4

So, I am currently working on an app that will do a simple countdown to a specific date; however, I am getting this error: "Value of type '(NSCalendar.Unit, Date, Date [NSCalendar.Options]) -> ()' has no member 'day' "
Any way to fix this to finish the app?
class ViewController: UIViewController {
#IBOutlet weak var CountdownText: UILabel!
let formatter = DateFormatter()
let userCalendar = NSCalendar.current
let requestedComponent: NSCalendar.Unit = [
NSCalendar.Unit.month,
NSCalendar.Unit.day,
NSCalendar.Unit.hour,
NSCalendar.Unit.minute,
NSCalendar.Unit.second,
]
func printTime()
{
formatter.dateFormat = "MM/dd/yy hh:mm:ss a"
let startTime = NSDate()
let endTime = formatter.date(from: "12/03/18 2:00:00 p")
func timeDifference (requestedComponent: NSCalendar.Unit, from: Date, to: Date, options: [NSCalendar.Options]) {}
CountdownText.text = "\(timeDifference.day) Days \(timeDifference.minute) Minutes \(timeDifference.second) Seconds"
}
}
You're trying to get call .day property from your function type which does and returns nothing.
I bet your function timeDifference should be somewhere outside the printTime function.
Believe you want to do something like following:
func printTime() {
formatter.dateFormat = "MM/dd/yy hh:mm:ss a"
let startTime = Date()
let endTime = formatter.date(from: "12/03/18 2:00:00 p")
let days = timeDifference(requestedComponent: .day, from: startTime, to: endTime, options: [])
let minutes = timeDifference(requestedComponent: .minute, from: startTime, to: endTime, options: [])
let seconds = timeDifference(requestedComponent: .second, from: startTime, to: endTime, options: [])
CountdownText.text = "\(days) Days \(minutes) Minutes \(seconds) Seconds"
}
func timeDifference (requestedComponent: NSCalendar.Unit, from: Date, to: Date, options: [NSCalendar.Options]) -> Int {
var calculatedValue = 0
// calculatings
return calculatedValue
}
You've declared a function named timeDifference() and you're trying to access it like a variable instance
Here's what you need to do:-
func timeDifference (requestedComponent: NSCalendar.Unit, from: Date, to: Date, options: [NSCalendar.Options]) {
//Do your things here
}
And call the function inside the variable this way:-
var text = "\(timeDifference(requestedComponent: .day, from: Date(), to: Date(), options: [.searchBackwards])) Days \(timeDifference(requestedComponent: .minute, from: Date(), to: Date(), options: [.searchBackwards])) Minutes \(timeDifference(requestedComponent: .second, from: Date(), to: Date(), options: [.searchBackwards])) Seconds"
Additionally, you can make your function parameters optional like this:-
func timeDifference (requestedComponent: NSCalendar.Unit, from: Date, to: Date, options: [NSCalendar.Options]? = nil) {}
The fourth parameter here is optional and the you don't have to provide an option every time. It's considered to be nil without input and it won't affect you in any way, unless you use it somewhere within the function, where you might have to handle it with an 'if' statement
(Just in case if it helps!)
NOTE:-
You can also return a "NSCalendar.Unit" from your function and use it for calculation, in which case, your usage pattern is right.
But I assume that the function here is written for the purpose of calculation and you're dealing with a label's value to display the calculated result. Dealing this all within a function is the sole purpose of a function, and hence I recommend doing this
You are trying to use function (with no return type) as NSCalendarComponent, hence it is showing you error. Instead, you and get start time and end time components like this:
func printTime(){
formatter.dateFormat = "MM/dd/yy hh:mm:ss a"
let calendar = Calendar.current
let startTime=calendar.dateComponents([.hour,.minute,.second], from: Date())
print("\(startTime.hour!):\(startTime.minute!):\(startTime.second!)")
let endTime = calendar.dateComponents([.hour,.minute,.second], from: formatter.date(from: "12/03/18 2:00:00 p") ?? Date())
print("\(endTime.hour!):\(endTime.minute!):\(endTime.second!)")
//do calculations here for time difference
}
and now you can calculate time difference and show it on countdown text.
First of all don't use NSCalendar and NSDate at all in Swift 3+.
Your code doesn't work because the function timeDifference has no return value and does nothing anyway.
You probably mean
let requestedComponents: Set<Calendar.Component> = [.month, .day, .hour, .minute, .second]
...
func timeDifference (requestedComponents: Set<Calendar.Component>, from: Date, to: Date) -> DateComponents {
return Calendar.current.dateComponents(requestedComponents, from: from, to: to)
}
...
let difference = timeDifference(requestedComponents: requestedComponents, from: startTime, to: endTime)
CountdownText.text = "\(difference.day!) Days \(difference.minute!) Minutes \(difference.second!) Seconds"
There is a more convenient way to calculate and display time differences, DateComponentsFormatter
func printTime()
{
let formatter = DateFormatter()
formatter.dateFormat = "MM/dd/yy hh:mm:ss a"
let startTime = Date()
let endTime = formatter.date(from: "12/03/18 2:00:00 p")!
let componentsFormatter = DateComponentsFormatter()
componentsFormatter.allowedUnits = [.hour, .minute, .second]
componentsFormatter.unitsStyle = .full
CountdownText.text = componentsFormatter.string(from: startTime, to: endTime)
}

Creating UILocalNotifications with CoreData attribute

In my iOS app I am trying to create a localNotification to notify the user 15 minutes prior to the event beginning. However I am stuck. I am using CoreData to store data. I have an Appointment object which can be created. A date attribute is associated with a Appointment object. I am really stuck with it. I do not know how to set up the timeInterval and the rest of the notification process.
I do not know how to set up the timeInterval from the time the Appointment is created to 15 minutes prior to when it begins.
Here is some of my code:
func scheduleNotifications() {
let content = UNMutableNotificationContent()
guard let client = client, let name = client.name, let formula = formula, let date = formula.date else { return }
content.title = "BookMe"
content.subtitle = ""
content.body = "Your appointment with \(name) will begin soon."
content.badge = 1
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: ??, repeats: false)
Edited: This is what I have but nothing is firing.
let date = formula.date
let fireDate = Calendar.current.date(byAdding: DateComponents(minute: -15), to: date as Date)
guard let timeInterval = fireDate?.timeIntervalSince(Date()) else { return }
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeInterval, repeats: false)
let request = UNNotificationRequest(identifier: self.timerUserNotificationIdentifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
As I understand it you are looking to find a time interval between right now and 15 minutes before some date so that you can fire a notification 15 minutes before that date.
Here's a quick example I knocked up in a playground
// Create a date in the future - this is what you get from your own data object and I'm creating it here just so I have a date.
let scheduledDate = Calendar.current.date(from: DateComponents(year: 2017, month: 09, day: 22, hour: 22))!
// Create a date 15 minutes earlier than your shcheduled date, this is when you want your notification to fire
let fireDate = Calendar.current.date(byAdding: DateComponents(minute: -15), to: scheduledDate)!
// Then just work out the time interval between the fire date and now to get the time interval
let timeInterval = fireDate.timeIntervalSince(Date())
Excuse the force unwrapping of the created dates, these are because it's an example, you should instead not use exclamation marks and handle errors gracefully.
edited to add
UNTimeIntervalNotificationTrigger, which you are trying to use requires a TimeInterval between now and the time you want to fire the notification. A TimeInterval is a Double that represents a number of seconds. In some cases, such as this one, it represents a delay, the number of seconds between now and the time you want to fire the the notification. In other cases it represents a date by the number of seconds from a fixed date. This fixed date is either timeIntervalSince1970 - "The interval between the date object and 00:00:00 UTC on 1 January 1970." Which is what you use for UNIX timestamps or timeIntervalSinceReferenceDate - "The interval between the date object and 00:00:00 UTC on 1 January 2001."
Whatever you do, resist the temptation to modify dates by adding or removing numbers of seconds directly, use DateComponents instead.

Repeat UserNotification every specific day of week for iOS 10

I'm working on local notification scheduling module for iOS 10 which repeats local notification for example every Sunday or every Monday..etc. Lets say i scheduled a notification for this date which is 2016-12-27 10:53:22 +0000 and using UNCalendarNotificationTrigger with repeat value equals true, the notification get triggered for ones in that date, and it doesn't repeat next week at the same time.
What could be the reason for that? and how is it possible to repeat every week for specific day in iOS 10.
Here is the code for creating local notification:
let content = UNMutableNotificationContent()
content.title = object.title
content.body = object.body
content.sound = UNNotificationSound.default()
let date = object.fireDate
let triggerDate = Calendar.current.dateComponents([.year,.month,.day,.hour,.minute,.second,], from: date as Date)
let trigger = UNCalendarNotificationTrigger(dateMatching: triggerDate,
repeats: true)
// Swift
let identifier = object.id
let request = UNNotificationRequest(identifier: identifier,
content: content, trigger: trigger)
localNotification.add(request, withCompletionHandler: { (error) in
if error != nil {
// Something went wrong
print(error!)
}else {
print("Reminder \(object.id) has been added successfully at \(object.fireDate)")
}
})
Update:
I have also discovered after the notification get fired at that date and to check that there is no more pending notification exist or to check if it has been rescheduled again or not. actually with repeat equals true, it has not been scheduled again for next week.
UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: { (notficiations) in
for localNotification in notficiations {
print(localNotification)
}
})
And the result was:
<UNNotificationRequest: 0x174223ca0; identifier: A1, content: <UNNotificationContent: 0x1742e2980; title: My Title, subtitle: (null), body: My Body, categoryIdentifier: , launchImageName: , peopleIdentifiers: (
), threadIdentifier: , attachments: (
), badge: (null), sound: <UNNotificationSound: 0x1740b1820>, hasDefaultAction: YES, shouldAddToNotificationsList: YES, shouldAlwaysAlertWhileAppIsForeground: NO, shouldLockDevice: NO, shouldPauseMedia: NO, isSnoozeable: NO, fromSnooze: NO, darwinNotificationName: (null), darwinSnoozedNotificationName: (null), trigger: <UNCalendarNotificationTrigger: 0x174223cc0; dateComponents: <NSDateComponents: 0x17415e140>
Calendar Year: 2016
Month: 12
Day: 27
Hour: 14
Minute: 46
Second: 15, repeats: YES>>
I don't know if its actually a bug in iOS or not.
Triggering date format is not proper to repeat notification in a day of a week.
Your current trigger date components includes year,month,day, etc so this notification repeat in each year in that particular month and day.Change trigger date like mentioned below to repeat notification in a day of a week.
let triggerDate = Calendar.current.dateComponents([.weekday,.hour,.minute], from: date as Date)
Here is approach which should work:
func addNotificationForAlarm(alarm: MyAlarm) {
let myAlarmNotifContent = UNMutableNotificationContent()
myAlarmNotifContent.title = "Reminder"
myAlarmNotifContent.body = alarm.activity
myAlarmNotifContent.sound = UNNotificationSound.default()
if alarm.repeatDays.count == 1 {
} else {
for index in 1...alarm.repeatDays.count {
createNotif(date: alarm.date, weekDay: index, content: myAlarmNotifContent)
}
}
}
private func createNotif(date: Date, weekDay: Int, content: UNNotificationContent) {
var dateComponent = DateComponents()
dateComponent.weekday = weekDay
dateComponent.hour = Calendar.current.dateComponents([.hour], from: date).hashValue
dateComponent.minute = Calendar.current.dateComponents([.minute], from: date).hashValue
let myAlarmTrigger = UNCalendarNotificationTrigger(dateMatching: dateComponent, repeats: true)
setupNotificationSettings()
let identifier = "Your-Notification"
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: myAlarmTrigger)
let center = UNUserNotificationCenter.current()
center.add(request, withCompletionHandler: { (error) in
if error != nil {
//TODO: Handle the error
}
})
}
Basically what I have found is that you can set a separate notification for each day you want the alarm to trigger. For example, you want every Monday and every Tuesday, so create a date component for each day and add it to the trigger. It is not perfect but is a solution that I think is better then calculating time intervals.

Constructing a date for getRequestedUpdateDateWithHandler:

I need to update my watchOS complication at midnight every day.
startOfDay is the beginning of the day (i.e., 12 AM today).
Should I add a day to the start of today like this?
func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
// Call the handler with the date when you would next like to be given the opportunity to update your complication content
let startOfDay = NSDate().startOfDay
let components = NSDateComponents()
components.day = 1
let startOfNextDay = NSCalendar.currentCalendar().dateByAddingComponents(components, toDate: startOfDay, options: NSCalendarOptions())
handler(startOfNextDay)
}
Or should I not add a day to the code, and just do something like this:
func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
// Call the handler with the date when you would next like to be given the opportunity to update your complication content
let startOfDay = NSDate().startOfDay
handler(startOfDay)
}
You'd want to advance the date one day, since you want your next requested update to occur at tomorrow's midnight. The first method would do what you want, but you can simplify it as follows:
let calendar = NSCalendar.currentCalendar()
let startOfDay = calendar.startOfDayForDate(NSDate())
let startOfNextDay = calendar.dateByAddingUnit(.Day, value: 1, toDate: startOfDay, options: NSCalendarOptions())!
The second code would return today's 12 AM, which would already be in the past.