So I have a timer that saves that saves the ending time using NSUserDefaults but I want to push that timer to the previous ViewController as well. The timer should be started on the second View Controller, and if you go back, or exit the app and reopen it, the timer should display. I have an idea of how do it with a Singleton DataService, but not quite sure how to put it all together. Here is my code as of now.
import UIKit
import UserNotifications
let stopTimeKey = "stopTimeKey"
class QOTDVC: UIViewController {
// TIMER VARIABLES
let timeInterval: Double = 89893
let defaults = UserDefaults.standard
var expirationDate = NSDate()
#IBOutlet weak var timerLabel: UILabel!
#IBAction func DoneWithQuestion(_ sender: AnyObject) {
self.dismiss(animated: true, completion: nil)
}
#IBOutlet weak var timerCounter: UILabel!
var timer: Timer?
var stopTime: Date?
override func viewDidLoad() {
super.viewDidLoad()
saveStopTime()
NotificationCenter.default.addObserver(self, selector: #selector(saveStopTime), name: NSNotification.Name(rawValue: "Date") , object: nil)
}
func alert(message: String, title: String = "") {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let OKAction = UIAlertAction(title: "OK", style: .default) {
UIAlertAction in
self.registerForLocalNotifications()
StartTimerInitiated()
}
alertController.addAction(OKAction)
self.present(alertController, animated: true, completion: nil)
}
func registerForLocalNotifications() {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound]) { (granted, error) in
if granted {
let content = UNMutableNotificationContent()
content.title = "Ready for the QOTD"
content.body = "You have 30 seconds to answer the question"
content.sound = UNNotificationSound.default()
let trigger = UNTimeIntervalNotificationTrigger.init(timeInterval: self.timeInterval , repeats: false)
let request = UNNotificationRequest(identifier: "myTrigger", content: content, trigger: trigger)
center.add(request)
}
}
}
func StartTimerInitiated() {
let time = Date(timeIntervalSinceNow: timeInterval)
if time.compare(Date()) == .orderedDescending {
startTimer(stopTime: time)
} else {
timerLabel.text = "timer date must be in future"
}
}
// MARK: Timer stuff
func startTimer(stopTime: Date) {
// save `stopTime` in case app is terminated
UserDefaults.standard.set(stopTime, forKey: stopTimeKey)
self.stopTime = stopTime
// start NSTimer
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(QOTDVC.handleTimer), userInfo: nil, repeats: true)
// start local notification (so we're notified if timer expires while app is not running)
}
func stopTimer() {
timer?.invalidate()
timer = nil
}
let dateComponentsFormatter: DateComponentsFormatter = {
let _formatter = DateComponentsFormatter()
_formatter.allowedUnits = [.hour, .minute, .second]
_formatter.unitsStyle = .positional
_formatter.zeroFormattingBehavior = .pad
return _formatter
}()
func handleTimer(timer: Timer) {
let now = Date()
if stopTime!.compare(now) == .orderedDescending {
timerLabel.text = dateComponentsFormatter.string(from: now, to: stopTime!)
} else {
stopTimer()
notifyTimerCompleted()
}
}
func notifyTimerCompleted() {
timerLabel.text = "Timer done!"
}
func saveStopTime() {
stopTime = UserDefaults.standard.object(forKey: stopTimeKey) as? Date
if let time = stopTime {
if time.compare(Date()) == .orderedDescending {
startTimer(stopTime: time)
} else {
notifyTimerCompleted()
}
}
stopTime = UserDefaults.standard.object(forKey: stopTimeKey) as? Date
}
Any help would be much appreciated. If you need any clarification, please let me know.
You are juggling several issues: Passing an object between view controllers, triggering code at some future time, and having a timer persist in the background.
As far as a timer that runs while your program is in the background, you can simply calculate the number of seconds between now and your target time and set a non-repeating timer for that number of seconds in the future. There's no reason to fire a repeating timer every second and do math to see if your time has passed yet. The way you're doing it will run the CPU hotter and drain your battery faster, so better to set up a single timer in the future.
Next, dealing with timers while in the background. The short answer is that you can't. Apps don't actually run in the background for very long. They quickly get suspended, which is a state where they are not getting any CPU time at all. You can ask for background time, but the system limits you to 3 minutes. You can play tricks to get more than 3 minutes of background time, but those tricks will cause Apple to reject your app, and would drain down the user's battery quite quickly if you did manage to sneak it by Apple. (When an app is running in the background the phone isn't able to go to sleep. THE CPU stays fully powered up, drawing a LOT more current than it does in the sleep state.
Finally, passing your timer from one view controller to the next. Yes, you can certainly use a singleton to make the timer a shared resource. You could also set up your two view controllers so that in the code that invokes the second from the first, you give the second view controller a delegate pointer back to the first, and set up a protocol that would let you pass the timer from the second view controller back to the first.
However, a timer calls a single target, so while you'll have access to the timer from either view controller using either the singleton pattern or the delegate pattern, the timer will still call the original target that you used when you set it up.
You could make your singleton the target of the timer, give the singleton a delegate, and have the singleton send a message to it's delegate when the timer fires. Then when you switch view controllers you could change the singleton's delegate to point to the new view controller.
Alternately you could record the "fire date" of your timer in your viewWillDisappear method, invalidate the timer, and create a new timer with that same fire date (actually you'd have to do some math to convert a fire date to a number of seconds, but it would be a single call.)
You could also use a local notification with a future fire date and set it up to play a sound. However, that won't invoke your program from the background unless the user responds to the notification.
Related
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.
So I am creating an app that has countdown timer. When the app quits I am using observers to know if the app is in background. If it is, I invalidate the timer and store the quit time in userDefaults. Then when the app comes back to foreground, I create a new timer and calculate the time that the app has been in background and subtract it from the total duration in order to get the elapsed time. When app goes to the background, I am storing the time in UserDefaults:
#objc func applicationDidEnterBackground() {
let defaults = UserDefaults.standard
let quitTime = Date()
defaults.set(quitTime, forKey: "quitTimeKey") //Storing the time of quit in UserDefaults
timer?.invalidate()
}
Then I create a new instance of timer when app enters foreground:
#objc func appEntersForeground() {
calculateTimeLeft()
if (timer == nil)
{
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(handleCountdown), userInfo: nil, repeats: true)
}
}
Then I check the elapsed time:
func checkElapsedTime() -> Double {
let currentTime = Date()
let appQuitTime = UserDefaults.standard.object(forKey: "quitTimeKey") as? Date ?? Date.distantFuture
let elapsedTime = currentTime.timeIntervalSince(appQuitTime)
return elapsedTime
}
I am also printing the time difference:
let timeDifference = checkElapsedTime()
print("timeDifference = \(timeDifference)")
Question: However, here is an issue. When I am using the app and I slide the notification center down and back up for not even a second, I get a timeDifference reading of few thousand seconds.
What could be the reason here? Is this iOS 12 bug? This only happens when I pull the notification center down when I am in the app.
Alright so I got it working. Basically when you are sliding down the notification center, you are calling applicationWillResignActive. So instead of calling applicationDidEnterBackground, I used applicationWillResignActive for the notification and it started working all fine!
I'm learning how to create a Pomodoro app, and am able to send notifications. However, I am totally clueless as to how to allow my timer label to update itself on reloading the app. Which means the timer works only when the app is open and not when it's in the foreground/background. Hoping to find a tutorial to learn from or just a quick answer code. Thanks!
Edit: Just to clear some misunderstandings, my app's Notification works fine with the timer, for example if 30mins is selected, the app would notify the user after 30mins. However, the problem is that when the app reopens, it resumes for example 29:57 seconds left on the timer label while the 30mins should have passed already.
*Added in AppDelegate*
var seconds = 0 //Timer countdown seconds
var currentDate = NSDate()
var setDate: Int = 0
func pauseApp(){
viewC.timer.invalidate() //invalidate timer
UserDefaults.standard.set(seconds, forKey: "current") //error occurs here where "Cannot assign value of type NSDate to type Timer"
setDate = UserDefaults.standard.integer(forKey: "current")
}
func startApp(){
let difference = currentDate.timeIntervalSince(NSDate() as Date) as Double
seconds = Int(Double(setDate) - difference)
viewC.updateTimer()
}
What someone suggests from a different thread is cancel the timer and store a NSDate when the app goes to the background. He stated we can use this notification to detect the app going to the background:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseApp", name: UIApplicationDidEnterBackgroundNotification, object: nil)
Then cancel the timer and store the date:
func pauseApp(){
self.stop() //invalidate timer
self.currentBackgroundDate = NSDate()
}
Use this notification to detect the user coming back:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "startApp", name: UIApplicationDidBecomeActiveNotification, object: nil)
Then calculate the difference from the stored date to the current date, update your counter and start the timer again:
func startApp(){
let difference = self.currentBackgroundDate.timeIntervalSinceDate(NSDate())
self.handler(difference) //update difference
self.start() //start timer }
However, I do not fully understand this code (namely, the difference between the "handler" and my own "seconds") as am new to programming... Hoping for an answer or helpful insight.
Solved: I managed to solve it myself from this video https://www.youtube.com/watch?v=V6ta24iBNBQ
Using this concept of timeDifference as well as UserDefaults.standard.set....
I managed to adapt it to my personal app with the code
You can call Timer to run the timmer when the view loads.
var runTimer : Timer?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
runTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(myFun), userInfo: nil, repeats: true)
}
func myFun(){
//do your logic
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
runTimer?.invalidate()
}
i am trying to do an application which can make a timer run in background.
here's my code:
let taskManager = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(self.scheduleNotification), userInfo: nil, repeats: true)
RunLoop.main.add(taskManager, forMode: RunLoopMode.commonModes)
above code will perform a function that will invoke a local notification.
this works when the app is in foreground, how can i make it work in the background?
i tried to put several print lines and i saw that when i minimize (pressed the home button) the app, the timer stops, when i go back to the app, it resumes.
i wanted the timer to still run in the background. is there a way to do it?
here's what i want to happen:
run app -> wait 10 secs -> notification received -> wait 10 secs -> notification received -> and back to wait and received again
that happens when in foreground. but not in background. pls help.
you can go to Capabilities and turn on background mode and active Audio. AirPlay, and picture and picture.
It really works . you don't need to set DispatchQueue .
you can use of Timer.
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (t) in
print("time")
}
Swift 4, Swift 5
I prefer to not run timer on background task, just compare a Date seconds between applicationDidEnterBackground and applicationWillEnterForeground.
func setup() {
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func applicationDidEnterBackground(_ notification: NotificationCenter) {
appDidEnterBackgroundDate = Date()
}
#objc func applicationWillEnterForeground(_ notification: NotificationCenter) {
guard let previousDate = appDidEnterBackgroundDate else { return }
let calendar = Calendar.current
let difference = calendar.dateComponents([.second], from: previousDate, to: Date())
let seconds = difference.second!
countTimer -= seconds
}
This works. It uses while loop inside async task, as suggested in another answer, but it is also enclosed within a background task
func executeAfterDelay(delay: TimeInterval, completion: #escaping(()->Void)){
backgroundTaskId = UIApplication.shared.beginBackgroundTask(
withName: "BackgroundSound",
expirationHandler: {[weak self] in
if let taskId = self?.backgroundTaskId{
UIApplication.shared.endBackgroundTask(taskId)
}
})
let startTime = Date()
DispatchQueue.global(qos: .background).async {
while Date().timeIntervalSince(startTime) < delay{
Thread.sleep(forTimeInterval: 0.01)
}
DispatchQueue.main.async {[weak self] in
completion()
if let taskId = self?.backgroundTaskId{
UIApplication.shared.endBackgroundTask(taskId)
}
}
}
}
A timer can run in the background only if both the following are true:
Your app for some other reason runs in the background. (Most apps don't; most apps are suspended when they go into the background.) And:
The timer was running already when the app went into the background.
Timer won't work in background. For background task you can check this link below...
https://www.raywenderlich.com/143128/background-modes-tutorial-getting-started
============== For Objective c ================
create Global uibackground task identifier.
UIBackgroundTaskIdentifier bgRideTimerTask;
now create your timer and add BGTaskIdentifier With it, Dont forget to remove old BGTaskIdentifier while creating new Timer Object.
[timerForRideTime invalidate];
timerForRideTime = nil;
bgRideTimerTask = UIBackgroundTaskInvalid;
UIApplication *sharedApp = [UIApplication sharedApplication];
bgRideTimerTask = [sharedApp beginBackgroundTaskWithExpirationHandler:^{
}];
timerForRideTime = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(timerTicked:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timerForRideTime forMode: UITrackingRunLoopMode];
Here this will work for me even when app goes in background.ask me if you found new problems.
You can achieve this by getting the time-lapse between background and foreground state of the app, here is the code snippet.
import Foundation
import UIKit
class CustomTimer {
let timeInterval: TimeInterval
var backgroundTime : Date?
var background_forground_timelaps : Int?
init(timeInterval: TimeInterval) {
self.timeInterval = timeInterval
}
private lazy var timer: DispatchSourceTimer = {
let t = DispatchSource.makeTimerSource()
t.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval)
t.setEventHandler(handler: { [weak self] in
self?.eventHandler?()
})
return t
}()
var eventHandler: (() -> Void)?
private enum State {
case suspended
case resumed
}
private var state: State = .suspended
deinit {
NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
timer.setEventHandler {}
timer.cancel()
resume()
eventHandler = nil
}
func resume() {
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackgroundNotification), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForegroundNotification), name: UIApplication.willEnterForegroundNotification, object: nil)
if state == .resumed {
return
}
state = .resumed
timer.resume()
}
func suspend() {
if state == .suspended {
return
}
state = .suspended
timer.suspend()
}
#objc fileprivate func didEnterBackgroundNotification() {
self.background_forground_timelaps = nil
self.backgroundTime = Date()
}
#objc fileprivate func willEnterForegroundNotification() {
// refresh the label here
self.background_forground_timelaps = Date().interval(ofComponent: .second, fromDate: self.backgroundTime ?? Date())
self.backgroundTime = nil
}
}
Use this class like;
self.timer = CustomTimer(timeInterval: 1)
self.timer?.eventHandler = {
DispatchQueue.main.sync {
var break_seconds = self.data.total_break_sec ?? 0
break_seconds += 1
if self.timer?.background_forground_timelaps != nil && self.timer?.backgroundTime == nil{
break_seconds += (self.timer?.background_forground_timelaps)!
self.timer?.background_forground_timelaps = nil
}
self.data.total_break_sec = String(break_seconds)
self.lblBreakTime.text = PRNHelper.shared.getPlainTimeString(time: TimeInterval(break_seconds))
}
}
self.timer?.resume()
This way I am able to get the timer right when resumed the app from background.
If 1 or 2 seconds threshold is acceptable this hack could be helpful.
UIApplication.didEnterBackgroundNotification
UIApplication.willEnterForegroundNotification
Stop Timer and Backup Date() on didEnterBackground.
Add Date() to the Backup date on willEnterForegraound to achieve total time.
Start Timer and Add total date to the Timer.
Notice: If user changed the date time of system it will be broken!
You dont really need to keep up with a NSTImer object. Every location update comes with its own timestamp.
Therefore you can just keep up with the last time vs current time and every so often do a task once that threshold has been reached:
if let location = locations.last {
let time = location.timestamp
guard let beginningTime = startTime else {
startTime = time // Saving time of first location time, so we could use it to compare later with subsequent location times.
return //nothing to update
}
let elapsed = time.timeIntervalSince(beginningTime) // Calculating time interval between first and second (previously saved) location timestamps.
if elapsed >= 5.0 { //If time interval is more than 5 seconds
//do something here, make an API call, whatever.
startTime = time
}
}
As others pointed out, Timer cannot make a method run in Background. What you can do instead is use while loop inside async task
DispatchQueue.global(qos: .background).async {
while (shouldCallMethod) {
self.callMethod()
sleep(1)
}
}
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.