4

In iOS system app clock, when I start a timer or stopwatch, quit the app, kill, and reopen it, the timer or stopwatch is still running.

How can I have a timer running in the background?
And how did Apple do it?

LinusGeffarth
  • 27,197
  • 29
  • 120
  • 174
FaiChou
  • 767
  • 7
  • 16
  • You need to restart the timer when your app runs again. – rmaddy May 30 '17 at 14:36
  • 2
    Save the time the stopwatch or timer went off and then calculate the time spent "in background" to set the new values when opening the app again. – LinusGeffarth May 30 '17 at 14:37
  • All they need it a `time_start` written somewhere (like `NSUserDefaults`) and they can simply continue after restart. – Rok Jarc May 30 '17 at 14:41
  • 1
    Assuming that you need to do something when timer runs out, another thing that can be done is to schedule "local notifications" using UILocalNotification or, starting with iOS10, UNNotificationRequest. One important factor here is that user has to allow your app to post local notifications, which is not guaranteed, so this should, probably, be in addition to storing timer data using NSUserDefaults or some other method. – Baglan May 30 '17 at 14:59
  • 1
    See https://stackoverflow.com/a/34497360/1271826 or https://stackoverflow.com/a/31642036/1271826 or https://stackoverflow.com/a/34862817/1271826 or https://stackoverflow.com/a/26405492/1271826 or – Rob May 30 '17 at 15:00

3 Answers3

14

It's possible through a token that identifies a request to run in the background.
Like this: var bgTask = UIBackgroundTaskIdentifier()

Here is how to use it:

var bgTask = UIBackgroundTaskIdentifier()
    bgTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {
        UIApplication.shared.endBackgroundTask(bgTask)
    })
    let timer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(notificationReceived), userInfo: nil, repeats: true)
    RunLoop.current.add(timer, forMode: RunLoopMode.defaultRunLoopMode)

I hope it would be useful!

Gabs
  • 422
  • 2
  • 4
1

Store the current timer and clock value inside a long attribute in seconds or milliseconds inside UserDefault when you close the app. I use a generic one i can share:

static let userDefaults = UserDefaults.standard

    public static func set<T: Hashable> (withValue value: T, andKey key: String) {
        userDefaults.set(value, forKey: key)
        userDefaults.synchronize()
    }
    public static func get(forKey key: String) -> Any? {
        if((userDefaults.object(forKey: key)) != nil){
            let data = userDefaults.object(forKey: key) as? Any
            return data
        }
        return nil
    }

When you start the application again, use the "get" method to get; lets say seconds and stored time. Then you can compare the current time to the stored one and calculate the seconds in between and add the time difference to the stored seconds and you can just set the timer to the presented value and keep it going.

Here is how you set it when you close the application:

NotificationCenter.default.addObserver(self, selector: #selector(storeTime), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)

func storeTime() {
     "UserDefaultClassName".set(withValue: "preferably dictionary", andKey: String)
}
Vollan
  • 1,887
  • 11
  • 26
  • Not sure why this was downvoted, because efficiency wise this is a better approach. Using background task to keep application alive to simply run countdown timer is ultimately wrong approach. – Arkadii Sep 09 '20 at 10:42
  • I'd add that you'll need to setup Local Notification for estimated timer's expiration date, so you won't miss it while app was in background or killed – Arkadii Sep 09 '20 at 10:43
-1

add this code to in appdelagate.swift

var backgroundUpdateTask: UIBackgroundTaskIdentifier = 0

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    return true
}

func applicationWillResignActive(application: UIApplication) {
    self.backgroundUpdateTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({
        self.endBackgroundUpdateTask()
    })
}

func endBackgroundUpdateTask() {
    UIApplication.sharedApplication().endBackgroundTask(self.backgroundUpdateTask)
    self.backgroundUpdateTask = UIBackgroundTaskInvalid
}

func applicationWillEnterForeground(application: UIApplication) {
    self.endBackgroundUpdateTask()
}