swift NSTimer in Background

左心房为你撑大大i 提交于 2019-11-27 02:23:50

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.

In Swift 3:

import UIKit
import UserNotifications

private let stopTimeKey = "stopTimeKey"

class ViewController: UIViewController {

    @IBOutlet weak var datePicker: UIDatePicker!
    @IBOutlet weak var timerLabel: UILabel!

    private var stopTime: Date?

    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()
            }
        }
    }

    private func registerForLocalNotifications() {
        if #available(iOS 10, *) {
            UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
                guard granted && error == nil else {
                    // display error
                    print("\(error)")
                    return
                }
            }
        } else {
            let types: UIUserNotificationType = [.badge, .sound, .alert]
            let settings = UIUserNotificationSettings(types: types, categories: nil)
            UIApplication.shared.registerUserNotificationSettings(settings)
        }
    }

    @IBAction func didTapStartButton(_ sender: AnyObject) {
        let time = datePicker.date
        if time > Date() {
            startTimer(time)
        } else {
            timerLabel.text = "timer date must be in future"
        }
    }

    // MARK: Timer stuff

    private var timer: Timer?

    private 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)
        }
    }

    private func stopTimer() {
        timer?.invalidate()
        timer = nil
    }

    private let dateComponentsFormatter: DateComponentsFormatter = {
        let _formatter = DateComponentsFormatter()
        _formatter.allowedUnits = [.hour, .minute, .second]
        _formatter.unitsStyle = .positional
        _formatter.zeroFormattingBehavior = .pad
        return _formatter
    }()

    // 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 `NSDateComponentsFormatter`, I'd suggest considering
    // `NSCalendar` method `components:fromDate:toDate:options:` to
    // get the number of hours, minutes, seconds, etc. between two
    // dates.

    func handleTimer(_ timer: Timer) {
        let now = Date()

        if stopTime! > now {
            timerLabel.text = dateComponentsFormatter.string(from: now, to: stopTime!)
        } else {
            stopTimer()
            notifyTimerCompleted()
        }
    }

    private func notifyTimerCompleted() {
        timerLabel.text = "Timer done!"
    }

}

Or in Swift 2:

private let stopTimeKey = "stopTimeKey"

class ViewController: UIViewController {

    @IBOutlet weak var datePicker: UIDatePicker!
    @IBOutlet weak var timerLabel: UILabel!

    var stopTime: NSDate?

    override func viewDidLoad() {
        super.viewDidLoad()

        registerForLocalNotifications()

        stopTime = NSUserDefaults.standardUserDefaults().objectForKey(stopTimeKey) as? NSDate
        if let time = stopTime {
            if time.compare(NSDate()) == .OrderedDescending {
                startTimer(time)
            } else {
                notifyTimerCompleted()
            }
        }
    }

    func registerForLocalNotifications() {
        let types: UIUserNotificationType = [.Badge, .Sound, .Alert]

        let settings = UIUserNotificationSettings(forTypes: types, categories: nil)

        UIApplication.sharedApplication().registerUserNotificationSettings(settings)
    }

    @IBAction func didTapStartButton(sender: AnyObject) {
        let time = datePicker.date
        if time.compare(NSDate()) == .OrderedDescending {
            startTimer(time)
        } else {
            timerLabel.text = "timer date must be in future"
        }
    }

    // MARK: Timer stuff

    var timer: NSTimer?

    func startTimer(stopTime: NSDate) {
        // save `stopTime` in case app is terminated

        NSUserDefaults.standardUserDefaults().setObject(stopTime, forKey: stopTimeKey)
        self.stopTime = stopTime

        // start NSTimer

        timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: "handleTimer:", userInfo: nil, repeats: true)

        // start local notification (so we're notified if timer expires while app is not running)

        let notification = UILocalNotification()
        notification.fireDate = stopTime
        notification.alertBody = "Timer finished!"
        UIApplication.sharedApplication().scheduleLocalNotification(notification)
    }

    func stopTimer() {
        timer?.invalidate()
        timer = nil
    }

    let dateComponentsFormatter: NSDateComponentsFormatter = {
        let _formatter = NSDateComponentsFormatter()
        _formatter.allowedUnits = [.Hour, .Minute, .Second]
        _formatter.unitsStyle = .Positional
        _formatter.zeroFormattingBehavior = .Pad
        return _formatter
    }()

    // I'm going to use `NSDateComponentsFormatter` 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 `NSDateComponentsFormatter`, I'd suggest considering
    // `NSCalendar` method `components:fromDate:toDate:options:` to 
    // get the number of hours, minutes, seconds, etc. between two 
    // dates.

    func handleTimer(timer: NSTimer) {
        let now = NSDate()

        if stopTime!.compare(now) == .OrderedDescending {
            timerLabel.text = dateComponentsFormatter.stringFromDate(now, toDate: 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).

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!