How do I set an NSCalendarUnitMinute repeatInterval on iOS 10 UserNotifications? - ios

In UILocalNotification we use NSCalendarUnitMinute like repetition ..... but I can't find in iOS 10 UserNotification doc ... How can I use NSCalendarUnitMinute like repetition in iOS 10 UserNotification?
here is the code which will schedule local notification at 8:30 pm and will repeat after every one minute.
UILocalNotification* localNotification = [[UILocalNotification alloc] init];
localNotification.fireDate = pickerDate;
localNotification.alertBody = self.textField.text;
localNotification.timeZone = [NSTimeZone defaultTimeZone];
localNotification.repeatInterval = NSCalendarUnitMinute;
localNotification.applicationIconBadgeNumber = [[UIApplication sharedApplication] applicationIconBadgeNumber] + 1;
[[UIApplication sharedApplication] scheduleLocalNotification:localNotification];

Although it appears that the old notifications framework had better support regarding this, it's only until, we realise that the new one is better!!
I had similar problems, and below is a sample of how the old notifications can go hand in hand with the new ones.
To be on a safer side, this is Swift2.3.
// Retrieve interval to be used for repeating the notification
private func retrieveRepeatInterval(repeatTemp: RepeatInterval) {
switch repeatTemp {
case .Never:
if #available(iOS 10.0, *) {
toDateComponents = NSCalendar.currentCalendar().components([.Hour, .Minute, .Day, .Month, .Year], fromDate: timeTemp!)
toDateComponents.second = 0
repeatNotification = false
} else {
repeatIntervalTemp = nil
}
case .EveryMinute:
if #available(iOS 10.0, *) {
toDateComponents.second = 0
repeatNotification = true
} else {
repeatIntervalTemp = .Minute
}
case .EveryHour:
if #available(iOS 10.0, *) {
toDateComponents = NSCalendar.currentCalendar().components([.Minute], fromDate: timeTemp!)
toDateComponents.second = 0
repeatNotification = true
} else {
repeatIntervalTemp = .Hour
}
case .EveryDay:
if #available(iOS 10.0, *) {
toDateComponents = NSCalendar.currentCalendar().components([.Hour, .Minute], fromDate: timeTemp!)
toDateComponents.second = 0
repeatNotification = true
} else {
repeatIntervalTemp = .Day
}
case .EveryWeek:
if #available(iOS 10.0, *) {
toDateComponents = NSCalendar.currentCalendar().components([.Hour, .Minute, .Weekday], fromDate: timeTemp!)
toDateComponents.second = 0
repeatNotification = true
} else {
repeatIntervalTemp = .WeekOfYear
}
case .EveryMonth:
if #available(iOS 10.0, *) {
toDateComponents = NSCalendar.currentCalendar().components([.Hour, .Minute, .Day], fromDate: timeTemp!)
toDateComponents.second = 0
repeatNotification = true
} else {
repeatIntervalTemp = .Month
}
case .EveryYear:
if #available(iOS 10.0, *) {
toDateComponents = NSCalendar.currentCalendar().components([.Hour, .Minute, .Day, .Month], fromDate: timeTemp!)
toDateComponents.second = 0
repeatNotification = true
} else {
repeatIntervalTemp = .Year
}
}
}
Although this isn't exhaustive, I've pretty much covered everything from a minute to a year.
And to be more detailed,
// RepeatInterval
enum RepeatInterval : String, CustomStringConvertible {
case Never = "Never"
case EveryMinute = "Every Minute"
case EveryHour = "Every Hour"
case EveryDay = "Every Day"
case EveryWeek = "Every Week"
case EveryMonth = "Every Month"
case EveryYear = "Every Year"
var description : String { return rawValue }
static let allValues = [Never, EveryMinute, EveryHour, EveryDay, EveryWeek, EveryMonth, EveryYear]
}
var repeatTemp: RepeatInterval?
var repeatIntervalTemp: NSCalendarUnit?
var timeTemp: NSDate?
var toDateComponents = NSDateComponents()
As this works for me, it should probably for the rest of you. Please try and let me know if there is an issue.
And, for the exact solution to this question, I'd advise the below.
(Probably, Apple didn't think about such a scenario, but it can be handled)
Schedule a notification for 8:30 PM
When the notification is triggered, remove it and schedule another notification with a different identifier, for the NSCalendarUnitMinute equivalent shown above.
Voila, it works!! (Or did it not?)

Use a UNCalendarNotificationTrigger with DateComponents instead of NSCalendar:
var date = DateComponents()
date.hour = 8
date.minute = 30
let trigger = UNCalendarNotificationTrigger(dateMatching: date, repeats: true)
let content = UNNotificationContent()
// edit your content
let notification = UNNotificationRequest(identifier: "myNotification", content: content, trigger: trigger)
You set up the date with a DateComponents instance and specify if the notification should repeat with the repeats parameter of the UNCalendarNotificationTrigger initializer.
UNCalendarNotificationTrigger Documentation
Just a heads up that there is a typo in the Apple Docs (as of 6/13). If you use NSDateComponents instead of DateComponents, you will need to explicitly cast your date as DateComponents in the dateMatching parameter.
In response to your comment, I don't believe the behavior you want (changing the frequency of your repeated notification) is supported by UNCalendarNotificationTrigger.

Bear with me this is my first post.
We needed the 1 minute repeat interval for our app in ios 10, and used as a workaround a combination the old and new framework.
scheduling the repeating local notifications with the old:
UILocalNotification *ln = [[UILocalNotification alloc] init];
...
ln.repeatInterval = kCFCalendarUnitMinute;
ln.userInfo = #{#"alarmID": ...}
...
[[UIApplication sharedApplication] scheduleLocalNotification:ln];
This will create and schedule the usual repeating LNs that will show up as UNNotification with a UNLegacyNotificationTrigger
I used a alarmID to connect old and new framework.
Try:
UNUserNotificationCenter* center = [UNUserNotificationCenter
currentNotificationCenter];
[center getPendingNotificationRequestsWithCompletionHandler:^(NSArray<UNNotificationRequest *> * _Nonnull requests) {
NSLog(#"----------------- PendingNotificationRequests %i -------------- ", (int)requests.count);
for (UNNotificationRequest *req in requests) {
UNNotificationTrigger *trigger = req.trigger;
NSLog(#"trigger %#", trigger);
}
}];
responding to actions and fire-ing notifications, manipulating the notification centre:
With the new framework UNUserNotificationCenter methods:
willPresentNotification:
didReceiveNotificationResponse:
Request authorization and category registration I did for both frameworks in the usual way.
Hope this helps

Related

Create weekly swift ios local notifications

I can create daily notifications but not weekly ones using swift 3 ios 10 User Notifications framework. Daily works fine but when I add in the .weekday parameter it doesn't send me a notification.
The code is:
for i in 1 ... 7
{
let center = UNUserNotificationCenter.current();
let content = UNMutableNotificationContent();
content.title = "Title";
content.body = "content";
content.sound = UNNotificationSound.default();
var dateComponents = DateComponents();
dateComponents.weekday = i;
// hours is 00 to 11 in string format
if (daynight == "am")
{
dateComponents.hour = Int(hours);
}
else
{
dateComponents.hour = Int(hours)! + 12;
}
// mins is 00 to 59 in string format
dateComponents.minute = Int(mins);
dateComponents.second = 0;
let date = Calendar.current.date(from: dateComponents);
// tiriggerDaily works without weekday for daily but adding weekday doesn't make it weekly
let triggerDaily = Calendar.current.dateComponents([.weekday, .hour, .minute, .second], from: date!);
let trigger = UNCalendarNotificationTrigger(dateMatching: triggerDaily, repeats: true);
let identifier = // a uuid;
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger);
center.add(request, withCompletionHandler:
{
(error) in
if let error = error
{
// Error happened
print("Error: ");
print(error);
}
});
Need to get code to figure out weekly. Used this as guidance: https://useyourloaf.com/blog/local-notifications-with-ios-10/
An example of what I want is this:
A user selects Monday, 11:25pm and it is a Monday at 11:23pm.
Then the user should get a notification in two minutes plus one the following week and so on.
Some other notes/questions:
1. Do you have to wait a week for weekly notifications to start? Like in the example above will it start the following week?
Just a thought since I can't get this working right yet.

How to trigger local notification fortnightly starting from a specific future date

I am trying to trigger UNUserNotification fortnightly which will start from a specific future day. I have triggered successfully with UNTimeIntervalNotificationTrigger. But my problem is I cannot set a specific start date here.
UNTimeIntervalNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:(14*24*3600) repeats: YES];
request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger];
I've tried with WeekdayOrdinal for UNCalendarNotificationTrigger but that does not work exactly with fortnight duration always.
Is there any way I can schedule local UNUserNotification fortnightly from a specific day in future?
UNNotificationRequest does not provide anything to trigger fortnight recurring.
With UNCalendarNotificationTrigger you can only trigger following types of recurring:
weekly, daily, monthly, yearly
switch repeatType {
case .daily:
newComponents.hour = components.hour
newComponents.minute = components.minute
break
case .weekly:
newComponents.hour = components.hour
newComponents.minute = components.minute
newComponents.weekday = components.weekday
break
case .monthly:
newComponents.hour = components.hour
newComponents.minute = components.minute
newComponents.day = components.day
break
case .none:
newComponents.hour = components.hour
newComponents.minute = components.minute
newComponents.day = components.day
newComponents.month = components.month
newComponents.year = components.year
break
case .annually:
newComponents.hour = components.hour
newComponents.minute = components.minute
newComponents.day = components.day
newComponents.month = components.month
break
}
Although you can easily use EventKit to create events and alarms within those events with all such recurring options you want like the fortnight, quarterly, half-yearly etc.
I have created this code to create events with multiple recurring types:
let eventStore = EKEventStore()
eventStore.requestAccess(to: .event, completion: { (granted, error) in
if (granted) && (error == nil) {
let event = EKEvent(eventStore: eventStore)
event.title = "Countdown title"
event.startDate = Date()
event.notes = "Countdown is complete!"
event.isAllDay = false
event.calendar = eventStore.defaultCalendarForNewEvents
var frequency : EKRecurrenceFrequency = EKRecurrenceFrequency.daily
var interval = 1
switch repeatType {
case .daily:
//Repeat every day
frequency = EKRecurrenceFrequency.daily
interval = 1
break
case .weekly:
//Repeat every week
frequency = EKRecurrenceFrequency.weekly
interval = 1
break
case .biweekly:
//Repeat every 2 weeks
frequency = EKRecurrenceFrequency.weekly
interval = 2
break
case .monthly:
//Repeat every month
frequency = EKRecurrenceFrequency.monthly
interval = 1
break
case .quarterly:
//Repeat every 3 months
frequency = EKRecurrenceFrequency.monthly
interval = 3
break
case .halfYearly:
//Repeat every 6 months
frequency = EKRecurrenceFrequency.monthly
interval = 6
break
default:
// Repeat every year
frequency = EKRecurrenceFrequency.yearly
interval = 1
}
let alarm : EKAlarm = EKAlarm(relativeOffset: TimeInterval(exactly: 0)!)
event.addAlarm(alarm)
if interval > 0 {
event.addRecurrenceRule(EKRecurrenceRule(recurrenceWith: frequency, interval: interval, end: nil))
}
do {
try eventStore.save(event, span: .thisEvent)
} catch let error as NSError {
print(error.localizedDescription)
return
}
} else {
print(error?.localizedDescription ?? "no error")
}
})
Please don't forget to import EventKit.
Try with triggerWithDateMatchingComponents method
Objective-C Example Code
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:86400]; // You can set your date here.
NSDateComponents *triggerDate = [[NSCalendar currentCalendar]
components:NSCalendarUnitYear +
NSCalendarUnitMonth + NSCalendarUnitDay +
NSCalendarUnitHour + NSCalendarUnitMinute +
NSCalendarUnitSecond fromDate:date];
UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:triggerDate repeats:NO];
Swift Example
let triggerDate = Calendar.current.date(byAdding: .day, value: 1, to: self)!
let trigger = UNCalendarNotificationTrigger(dateMatching: triggerDate, repeats: false)

How to format time intervals for user display (social network like) in swift?

I have a time interval, say, 12600, which is equivalent to 3 hours 30 minutes. How could I format any such time interval so that only the highest part of the interval (for example in this case figure, the hours) is kept and have the correct locale abbreviation be appended to the number. For example 10m (10 minutes), 3d (3 days), 1y (1 years).
EDIT: Here are some examples:
Time interval in: 90000 Whole string: 1d String out: 1d
Time interval in: 900 Whole string: 15m String out: 15m
Time interval in: 13500 Whole String: 3h 45m String out: 4h
As a general rule, apply the normal rounding rules (3.4 rounds down, 3.6 rounds up).
If you are targeting newer OS versions (iOS 13.5+, OS X 10.15+), you can use RelativeDateTimeFormatter:
let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .named
for d in [-12600.0, -90000.0, -900.0, 13500.0] {
let str = formatter.localizedString(fromTimeInterval: d)
print("\(d): \(str)")
}
// Output
-12600.0: 3 hours ago
-90000.0: yesterday
-900.0: 15 minutes ago
13500.0: in 3 hours
For older OS versions, use DateComponentFormatter, available since iOS 8:
func format(duration: TimeInterval) -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.day, .hour, .minute, .second]
formatter.unitsStyle = .abbreviated
formatter.maximumUnitCount = 1
return formatter.string(from: duration)!
}
for d in [12600.0, 90000.0, 900.0, 13500.0] {
let str = format(duration: d)
print("\(d): \(str)")
}
This prints:
12600.0: 4h
90000.0: 1d
900.0: 15m
13500.0: 4h
Just in case anyone wants it.. Swift 4
extension TimeInterval {
func format(using units: NSCalendar.Unit) -> String? {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = units
formatter.unitsStyle = .abbreviated
formatter.zeroFormattingBehavior = .pad
return formatter.string(from: self)
}
}
Example usage:
let value:TimeInterval = 12600.0
print("\(value.format(using: [.hour, .minute, .second])!)")
and the result will be:
3h 30m 0s
Swift 3 extension:
extension TimeInterval {
func format() -> String? {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.day, .hour, .minute, .second, .nanosecond]
formatter.unitsStyle = .abbreviated
formatter.maximumUnitCount = 1
return formatter.string(from: self)
}
}
Take a look at the NSDateComponentsFormatter class. It lets you calculate whatever units you want either using 2 dates or using an NSTimeInterval, and supports different languages and locales automatically. There have been a couple of posts here in SO on the subject.
You can use NSDate and NSCalendar. You can say something like:
let timeInterval:Double = 12600
let calendar = NSCalendar.currentCalendar()
let date = NSDate(timeInterval: -timeInterval, sinceDate: NSDate())
let components = calendar.components([.Year,.Day,.Hour, .Minute, .Second, .Nanosecond], fromDate: date, toDate: NSDate(), options: [])
let hour = components.hour //3
let minute = components.minute //30
Per duncan's and rmaddy's suggestions use NSDateComponentsFormatter
I created a function for you! I hope you like it. And this is super easy to implement and very customizable.
func totime(time: Double) -> (String) {
var timex = time
var fancytime: String = "a while"
if time < 61 {
fancytime = "\(timex)s"
} else if time < 3601 {
timex = timex/60
timex = round(timex)
fancytime = "\(timex)m"
} else if time < 86401 {
timex = timex/3600
timex = round(timex)
fancytime = "\(timex)h"
} else if Double(time) < 3.15576E+07 {
timex = timex/86400
timex = round(timex)
fancytime = "\(timex)d"
} else {
fancytime = "more than one year"
}
fancytime = fancytime.stringByReplacingOccurrencesOfString(".0", withString: "")
return fancytime
}
Tested, and it works flawlessly:
print(totime(90000)) // prints "1d"
print(totime(900)) // prints "15m"
print(totime(13500)) // prints "4h"
Just call it with totime(Double).

Having trouble scheduling local notification to fire at specific time and date in swift

I am trying to schedule a notification at a certain time in swift. My TimeString is a string that looks like 08:32 or 12:23. When I try to fire on this specific date and time, the notification only fires on the specific date however not the specific time. For instance if I change the year to 2016 it won't fire. If I change the month it won't fire. Yet regardless of what time I put in, if the date matches the current date, I am writing this on 1/28/2015, the notification fires regardless of time, In fact it fires as soon as I close the application as my code is in
func applicationDidEnterBackground(application: UIApplication) {
}
My code does compile so I don't think there is an error in my code.
Is there any way to fire a notification at a specific time and not just a date in swift, and if so how do I implement this.
var timeString : String = "2015-01-28 " + TimeString;
println(timeString);
let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
formatter.timeZone = NSTimeZone(abbreviation: "GMT");
var date = formatter.dateFromString(timeString)
var localNotification:UILocalNotification = UILocalNotification()
localNotification.alertAction = "Block"
localNotification.alertBody = block.description() + " Started";
localNotification.fireDate = date
UIApplication.sharedApplication().scheduleLocalNotification(localNotification)
Try using this approach to set your date:
let calendar = NSCalendar.currentCalendar()
let calendarComponents = NSDateComponents()
calendarComponents.hour = 8 //you can get you time components
calendarComponents.second = 0 //from your string rather than
calendarComponents.minute = 0 //using fixed values
calendarComponents.day = 28
calendarComponents.year = 2015
calendarComponents.month = 1
let dateToFire = calendar.dateFromComponents(calendarComponents)
It worked for me. Hope this helps. :)

UIDatePicker restrict hours but not date

I came across a situation where I needed to restrict a UIDatePicker's selected hour, but still allow free selection of the day. This would be useful if you wanted to allow a user to select a date/time during set business hours. I found something that was close to what I was wanting to do by alerting the user that their selection was bad, but didn't actually change the date on the picker, so I wanted to share my solution Q&A-style.
This particular example will not allow selection of times before 7:00am or after 9:59pm. Selection of an "invalid" time will immediately slide the UIDatePicker back to the closest valid time on the respective end of the spectrum (for example, selection of 10:02pm will immediately slide back to 9:59pm)
- (void)datePickerChanged
{
NSDateComponents *components = [[NSCalendar currentCalendar] components:NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit|NSHourCalendarUnit|NSMinuteCalendarUnit fromDate:datePicker.date];
if([components hour] < 7)
{
[components setHour:7];
[components setMinute:0];
[datePicker setDate:[[NSCalendar currentCalendar] dateFromComponents:components]];
}
else if([components hour] > 21)
{
[components setHour:21];
[components setMinute:59];
[datePicker setDate:[[NSCalendar currentCalendar] dateFromComponents:components]];
}
}
Edit: As #DuncanC suggested in the comments, feedback to the user should probably be included, such as a label saying "Only times between 7:00am and 9:59pm can be used"
In Swift2.0:
func datePickerChanged() {
let components = NSCalendar.currentCalendar().components(
[NSCalendarUnit.Year, NSCalendarUnit.Month, NSCalendarUnit.Day, NSCalendarUnit.WeekOfYear, NSCalendarUnit.Hour, NSCalendarUnit.Minute, NSCalendarUnit.Second, NSCalendarUnit.Weekday, NSCalendarUnit.WeekdayOrdinal, NSCalendarUnit.WeekOfYear],
fromDate: datePickerInstance.date)
if components.hour < 7 {
components.hour = 7
components.minute = 0
datePickerInstance.setDate(NSCalendar.currentCalendar().dateFromComponents(components)!, animated: true)
}
else if components.hour > 21 {
components.hour = 21
components.minute = 59
datePickerInstance.setDate(NSCalendar.currentCalendar().dateFromComponents(components)!, animated: true)
}
else {
print("Everything is good.")
}
}
If you want to actually limit the hours that are displayed, I built a custom picker for that purpose. It's a subclass of UIPickerView and it replicates the functionality of UIDatePicker in countDownTimer mode, while adding support to set maxTimeInterval.
You use it like this:
GSTimeIntervalPicker *picker = [[GSTimeIntervalPicker alloc] init];
picker.maxTimeInterval = (3600 * 3); // set the limit
picker.minuteInterval = 5; // the step. Default is 1 min.
picker.timeInterval = (3600 * 1.5); // 1 h 30 minutes
picker.onTimeIntervalChanged = ^(NSTimeInterval newTimeInterval) {
// Use the value
};
Available on GitHub under MIT license. Blog post here.
In Swift 4:
func datePickerChanged() {
var components = Calendar.current.dateComponents([.hour, .minute, .month, .year, .day], from: datePicker.date)
if components.hour! < 7 {
components.hour = 7
components.minute = 0
datePicker.setDate(Calendar.current.date(from: components)!, animated: true)
}
else if components.hour! > 21 {
components.hour = 21
components.minute = 59
datePicker.setDate(Calendar.current.date(from: components)!, animated: true)
}
else {
print("Everything is fine!")
}
}

Resources