I have a function scheduleFutureLocalNotifications() in Swift code that creates 64 UILocalNotification that fire in the future.
Originally the function was called at viewDidLoad(), but this caused delays when starting the app.
Next, the function was called during the active application, but this caused unpredictable pauses or lagging of the user interface.
Finally, the function was moved to trigger when the app transitions to the background after receiving a UIApplicationDidEnterBackground notification, but this causes iOS to briefly lag as the local notifications are prepared in the background. This appears more evident on older devices.
Question:
1 - How can I reduce lag and improve user interface responsiveness
creating local notifications?
2 - What better techniques can be employed to schedule the 64
notifications?
3 - What other better times could the function scheduleFutureLocalNotifications() be called?
Code:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
scheduleFutureLocalNotifications()
}
func scheduleFutureLocalNotifications() {
// Remove all previous local notifications
let application = UIApplication.sharedApplication()
application.cancelAllLocalNotifications()
// Set new local notifications to fire over the coming 64 days
for nextLocalNotification in 1...64 {
// Add calendar day
let addDayComponent = NSDateComponents()
addDayComponent.day = nextLocalNotification
let calendar = NSCalendar.currentCalendar()
let nextDate = calendar.dateByAddingComponents(addDayComponent, toDate: NSDate(), options: [])
// Set day components for next fire date
let nextLocalNotificationDate = NSCalendar.currentCalendar()
let components = nextLocalNotificationDate.components([.Year, .Month, .Day], fromDate: nextDate!)
let year = components.year
let month = components.month
let day = components.day
// Set notification fire date
let componentsFireDate = NSDateComponents()
componentsFireDate.year = year
componentsFireDate.month = month
componentsFireDate.day = day
componentsFireDate.hour = 0
componentsFireDate.minute = 0
componentsFireDate.second = 5
let fireDateLocalNotification = calendar.dateFromComponents(componentsFireDate)!
// Schedule local notification
let localNotification = UILocalNotification()
localNotification.fireDate = fireDateLocalNotification
localNotification.alertBody = ""
localNotification.alertAction = ""
localNotification.timeZone = NSTimeZone.defaultTimeZone()
localNotification.repeatInterval = NSCalendarUnit(rawValue: 0)
localNotification.applicationIconBadgeNumber = nextLocalNotification
application.scheduleLocalNotification(localNotification)
}
}
}
You can dispatch the function, asynchronously, onto another queue. This will ensure that the main queue isn't blocked performing the scheduling and will prevent the UI from becoming unresponsive:
override func viewDidLoad() {
super.viewDidLoad()
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
scheduleFutureLocalNotifications()
}
}
Related
I just want to ask is this even possible?
I've made simple method which as I suspect will fire notification every 1st day of month at 12:00
What I want to do is fires notifications 1st,2nd,3rd day of every month at for ex. 8:00 am and 12:00
func scheduleLocalNotification() {
let calendar = NSCalendar.autoupdatingCurrent
var calendarComponents = DateComponents()
calendarComponents.day = 1
calendarComponents.hour = 12
calendarComponents.minute = 00
let trigger = UNCalendarNotificationTrigger(dateMatching: calendarComponents, repeats: true)
let localNotification = UILocalNotification()
localNotification.alertBody = "Hey, you must go shopping, remember?"
localNotification.soundName = UILocalNotificationDefaultSoundName
}
In my app, i have implemented functionality to check app version using bundleVersion String. Now, i want to run this function everyday at 8:00 a.m. This is kiosk based app which does not go into background. So, app would be active all the time.
I am using UILocalnotification to schedule a notification for that time. Now, my app has other UILocalnotification as well. I am not sure how can i identify notifications in app delegate didReceiveLocalNotification() method.
My method to schedule notification is below
func scheduleNotification() {
//UIApplication.sharedApplication().cancelAllLocalNotifications()
let notif = UILocalNotification()
let calendar = NSCalendar.currentCalendar()
let date = NSDate()
var calendarComponents = NSDateComponents()
calendarComponents = calendar.components([.Day,.Month,.Year], fromDate: date)
let day = calendarComponents.day
let month = calendarComponents.month
let year = calendarComponents.year
calendarComponents.day = day
calendarComponents.month = month
calendarComponents.year = year
calendarComponents.hour = 8
calendarComponents.second = 0
calendarComponents.minute = 0
calendar.timeZone = NSTimeZone.systemTimeZone()
let dateToFire = calendar.dateFromComponents(calendarComponents)
notif.fireDate = dateToFire
notif.timeZone = NSTimeZone.systemTimeZone()
notif.repeatInterval = NSCalendarUnit.NSWeekdayCalendarUnit
UIApplication.sharedApplication().scheduleLocalNotification(notif)
}
Any idea would be appreciated.
Following method could help you to execute any task at regular interval , i used this method to call webservice at regular interval to provide searching functionality :
let debounceTimer : NSTimer?
func test() {
if let timer = debounceTimer {
timer.invalidate()
}
debounceTimer = NSTimer(timeInterval: 0.3, target: self, selector: Selector("putmethodnamewhichneedstocall"), userInfo: nil, repeats: false)
NSRunLoop.currentRunLoop().addTimer(debounceTimer!, forMode: "NSDefaultRunLoopMode")
}
For specific time daily Refer to this SO link :Repeating local notification daily at a set time with swift
Hope it helps.
I have come across a lot of issues with how to handle NSTimer in background here on stack or somewhere else. I've tried one of all the options that actually made sense .. to stop the timer when the application goes to background with
NSNotificationCenter.defaultCenter().addObserver(self, selector: "appDidEnterBackground", name: UIApplicationDidEnterBackgroundNotification, object: nil)
and
NSNotificationCenter.defaultCenter().addObserver(self, selector: "appDidBecomeActive", name: UIApplicationWillEnterForegroundNotification, object: nil)
At first I thought that my problem is solved, I just saved the time when the app did enter background and calculated the difference when the app entered foreground .. but later I noticed that the time is actually postponed by 3, 4 , 5 seconds .. that it actually is not the same .. I've compared it to the stopwatch on another device.
Is there REALLY any SOLID solution to running an NSTimer in background?
You shouldn't be messing with any adjustments based upon when it enters background or resumes, but rather just save the time that you are counting from or to (depending upon whether you are counting up or down). Then when the app starts up again, you just use that from/to time when reconstructing the timer.
Likewise, make sure your timer handler is not dependent upon the exact timing that the handling selector is called (e.g. do not do anything like seconds++ or anything like that because it may not be called precisely when you hope it will), but always go back to that from/to time.
Here is an example of a count-down timer, which illustrates that we don't "count" anything. Nor do we care about the time elapsed between appDidEnterBackground and appDidBecomeActive. Just save the stop time and then the timer handler just compares the target stopTime and the current time, and shows the elapsed time however you'd like.
For example:
import UIKit
import UserNotifications
private let stopTimeKey = "stopTimeKey"
class ViewController: UIViewController {
#IBOutlet weak var datePicker: UIDatePicker!
#IBOutlet weak var timerLabel: UILabel!
private weak var timer: Timer?
private var stopTime: Date?
let dateComponentsFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute, .second]
formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = .pad
return formatter
}()
override func viewDidLoad() {
super.viewDidLoad()
registerForLocalNotifications()
stopTime = UserDefaults.standard.object(forKey: stopTimeKey) as? Date
if let time = stopTime {
if time > Date() {
startTimer(time, includeNotification: false)
} else {
notifyTimerCompleted()
}
}
}
#IBAction func didTapStartButton(_ sender: Any) {
let time = datePicker.date
if time > Date() {
startTimer(time)
} else {
timerLabel.text = "timer date must be in future"
}
}
}
// MARK: Timer stuff
private extension ViewController {
func registerForLocalNotifications() {
if #available(iOS 10, *) {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
guard granted, error == nil else {
// display error
print(error ?? "Unknown error")
return
}
}
} else {
let types: UIUserNotificationType = [.alert, .sound, .badge]
let settings = UIUserNotificationSettings(types: types, categories: nil)
UIApplication.shared.registerUserNotificationSettings(settings)
}
}
func startTimer(_ stopTime: Date, includeNotification: Bool = true) {
// save `stopTime` in case app is terminated
UserDefaults.standard.set(stopTime, forKey: stopTimeKey)
self.stopTime = stopTime
// start Timer
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(handleTimer(_:)), userInfo: nil, repeats: true)
guard includeNotification else { return }
// start local notification (so we're notified if timer expires while app is not running)
if #available(iOS 10, *) {
let content = UNMutableNotificationContent()
content.title = "Timer expired"
content.body = "Whoo, hoo!"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: stopTime.timeIntervalSinceNow, repeats: false)
let notification = UNNotificationRequest(identifier: "timer", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(notification)
} else {
let notification = UILocalNotification()
notification.fireDate = stopTime
notification.alertBody = "Timer finished!"
UIApplication.shared.scheduleLocalNotification(notification)
}
}
func stopTimer() {
timer?.invalidate()
}
// I'm going to use `DateComponentsFormatter` to update the
// label. Update it any way you want, but the key is that
// we're just using the scheduled stop time and the current
// time, but we're not counting anything. If you don't want to
// use `DateComponentsFormatter`, I'd suggest considering
// `Calendar` method `dateComponents(_:from:to:)` to
// get the number of hours, minutes, seconds, etc. between two
// dates.
#objc func handleTimer(_ timer: Timer) {
let now = Date()
if stopTime! > now {
timerLabel.text = dateComponentsFormatter.string(from: now, to: stopTime!)
} else {
stopTimer()
notifyTimerCompleted()
}
}
func notifyTimerCompleted() {
timerLabel.text = "Timer done!"
}
}
By the way, the above also illustrates the use of a local notification (in case the timer expires while the app isn't currently running).
For Swift 2 rendition, see previous revision of this answer.
Unfortunately, there is no reliable way to periodically run some actions while in background. You can make use of background fetches, however the OS doesn't guarantee you that those will be periodically executed.
While in background your application is suspended, and thus no code is executed, excepting the above mentioned background fetches.
I am looking for a medhold to add 1 for each day to the badge icon. I have tried multiple ways, but none of them have updated the icon badge number.
override func viewDidLoad() {
super.viewDidLoad()
let localNotification:UILocalNotification = UILocalNotification()
localNotification.fireDate = datePicker.date
localNotification.alertBody = nil;
localNotification.alertAction = nil;
localNotification.timeZone = NSTimeZone.defaultTimeZone()
localNotification.repeatInterval = NSCalendarUnit.Day
//Add one to the icon badge number
localNotification.applicationIconBadgeNumber = UIApplication.sharedApplication().applicationIconBadgeNumber + 1;
UIApplication.sharedApplication().scheduleLocalNotification(localNotification)
So I would choose another approach. My idea is to set a self-repeating timer that fires every 86,400s (24hrs/1d) and sends a notification. If that works the badge count would increase automatically by one.
//!pseudo-code!
let timer = NSTimer.scheduledTimerWithTimeInterval(864000, target: self, selector: "sendNotification:", userInfo: nil, repeats: true)
func sendNotification(timer: NSTimer) {
//initialize and send your notification w/ repeatCount: 0 and fireDate: now
//hide the banner so the user does not visually see the notification if you wish
}
I am not sure whether this will work if the app is not running but it may be worth a try.
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?