运行 phone 休眠时的计时器

Running a timer when the phone is sleeping

我正在构建一个应用程序,我需要一个计时器来 运行 如果用户将屏幕发送到后台,或者如果他们将 phone 置于睡眠状态并再次打开它。我需要计时器继续运行。

我尝试记录我退出并再次进入的时间,减去两者并将其添加到 运行ning 计数中,它似乎在 Xcode 模拟器上工作正常但是当我 运行 它在我的 phone 上时它不起作用。有任何想法吗?

这里是参考代码
计时器以一个按钮开始,我没有包括那部分,但它只是一个调用 timer.fire() 函数的简单 IBAction。

var time = 0.0
var timer = Timer()
var exitTime : Double = 0
var resumeTime : Double = 0

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(true)
    exitTime = Date().timeIntervalSinceNow
}

override func awakeFromNib() {
    super.awakeFromNib()
    resumeTime = Date().timeIntervalSinceNow
    time += (resumeTime-exitTime)
    timer.fire()
}


func startTimer() {
    if !isTimeRunning {
        timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: 
        #selector(WorkoutStartedViewController.action), userInfo: nil, repeats: true)
        isTimeRunning = true
    }
}

func pauseTimer() {
    timer.invalidate()
    isTimeRunning = false
}

@objc func action()
{
    time += 0.1
    timerLabel.text = String(time)
    let floorCounter = Int(floor(time))
    let hour = floorCounter/3600
    let minute = (floorCounter % 3600)/60
    var minuteString = "\(minute)"
    if minute < 10 {
        minuteString = "0\(minute)"
    }

    let second = (floorCounter % 3600) % 60
    var secondString = "\(second)"
    if second < 10 {
        secondString = "0\(second)"
    }

    if time < 3600.0 {
        timerLabel.text = "\(minuteString):\(secondString)"
    } else {
        timerLabel.text = "\(hour):\(minuteString):\(secondString)"
    }

}

你的想法是正确的,但我看到的第一个问题是 viewWillDissapear 仅在你离开视图控制器转到新的 viewController 时调用 - 当应用离开视图进入后台(按下主页按钮)

我相信您正在寻找的回调函数是 UIApplication.willResignActive(转到后台)和 UIApplication.didBecomeActive(应用重新打开)

您可以在 AppDelegate 中访问这些方法,或者您可以在视图控制器上设置它们,这里混合了您的代码和一些更改以在一个初始 VC 上生成工作示例:

import UIKit
import CoreData

class ViewController: UIViewController {

    @IBOutlet weak var timerLabel: UILabel!

    var time = 0.0
    var timer = Timer()
    var exitTime : Date?    // Change to Date
    var resumeTime : Date?    // Change to Date
    var isTimeRunning = false

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        startTimer()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        NotificationCenter.default.addObserver(self,
        selector: #selector(applicationDidBecomeActive),
        name: UIApplication.didBecomeActiveNotification,
        object: nil)
        // Add willResign observer
        NotificationCenter.default.addObserver(self,
        selector: #selector(applicationWillResign),
        name: UIApplication.willResignActiveNotification,
        object: nil)
    }

    override func viewWillDisappear(_ animated: Bool) {
        // Remove becomeActive observer
        NotificationCenter.default.removeObserver(self,
                                                  name: UIApplication.didBecomeActiveNotification,
                                                  object: nil)
        // Remove becomeActive observer
        NotificationCenter.default.removeObserver(self,
                                                  name: UIApplication.willResignActiveNotification,
                                                  object: nil)

    }

    func startTimer() {
        if !isTimeRunning {
            timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:
                #selector(self.action), userInfo: nil, repeats: true)
            isTimeRunning = true
        }
    }

    @objc func action() {
        time += 0.1
        timerLabel.text = String(time)
        let floorCounter = Int(floor(time))
        let hour = floorCounter/3600
        let minute = (floorCounter % 3600)/60
        var minuteString = "\(minute)"
        if minute < 10 {
            minuteString = "0\(minute)"
        }

        let second = (floorCounter % 3600) % 60
        var secondString = "\(second)"
        if second < 10 {
            secondString = "0\(second)"
        }

        if time < 3600.0 {
            timerLabel.text = "\(minuteString):\(secondString)"
        } else {
            timerLabel.text = "\(hour):\(minuteString):\(secondString)"
        }
    }

    @objc func applicationDidBecomeActive() {
        // handle event
        lookForActiveTimers()
    }

    func lookForActiveTimers() {

        var timers = [NSManagedObject]()

        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            return
        }
        let managedContext = appDelegate.persistentContainer.viewContext
        let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Timers")

        //3
        do {
            timers = try managedContext.fetch(fetchRequest)
            print("timers: \(timers)")

            var activeTimer: NSManagedObject?

            for timer in timers {
                if let active = timer.value(forKey: "active") as? Bool {
                    if active {
                        activeTimer = timer
                    }
                }
            }

            if let activeTimer = activeTimer {

                // Handle active timer (may need to go to a new view)
                if let closeDate = activeTimer.value(forKey: "appCloseTime") as? Date {

                    if let alreadyTimed = activeTimer.value(forKey: "alreadyTimed") as? Double {

                        let now = Date()
                        let difference = now.timeIntervalSince(closeDate)

                        // Handle set up again here
                        print("App opened with a difference of \(difference) and already ran for a total of \(alreadyTimed) seconds before close")

                        time = alreadyTimed + difference
                        startTimer()

                    }
                }

            } else {
                print("We dont have any active timers")
            }

            // Remove active timers because we reset them up
            for timer in timers {
                managedContext.delete(timer)
            }
            do {
                print("deleted")
                try managedContext.save() // <- remember to put this :)
            } catch {
                // Do something... fatalerror
            }

        } catch let error as NSError {
          print("Could not fetch. \(error), \(error.userInfo)")
        }
    }

    @objc func applicationWillResign() {
        // handle event
        saveActiveTimer()
    }


    func saveActiveTimer() {
        if isTimeRunning {
            // Create a new alarm object
            guard let appDelegate =
              UIApplication.shared.delegate as? AppDelegate else {
              return
            }

            let context = appDelegate.persistentContainer.viewContext
            if let entity = NSEntityDescription.entity(forEntityName: "Timers", in: context) {

                let newTimer = NSManagedObject(entity: entity, insertInto: context)
                newTimer.setValue(true, forKey: "active")

                let now = Date()
                newTimer.setValue(now, forKey: "appCloseTime")
                newTimer.setValue(self.time, forKey: "alreadyTimed")

                do {
                   try context.save()
                    print("object saved success")
                  } catch {
                   print("Failed saving")
                }
            }
        }
    }
}

编辑 - 这是 xCode 11.3 和物理设备 iOS 13.2 上的完整测试和工作代码 - 你必须弄清楚如何根据你的按钮启动和停止计时器 -但是这个例子只是在第一次打开应用程序时启动计时器,并且永远不会停止或重置它。

您可以通过创建一个新的单视图 xCode 项目并用上面的代码替换它为您创建的第一个视图控制器中的代码来重现这一点。然后创建一个标签以附加到 VC

上的插座 timerLabel
  • 同时确保在创建新项目时在项目中启用 CoreData * 然后在 xcdatamodel 文件中设置实体和属性:

希望对您有所帮助