在手机休眠时运行计时器

Posted

技术标签:

【中文标题】在手机休眠时运行计时器【英文标题】:Running a timer when the phone is sleeping 【发布时间】:2020-08-31 10:06:13 【问题描述】:

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

我尝试记录退出并再次输入的时间,减去两者并将其添加到运行计数中,它似乎在 Xcode 模拟器上运行良好,但当我在手机上运行它时却没有工作。有任何想法吗?

这是供参考的代码。 计时器从一个按钮开始,我没有包含该部分,但它只是一个调用 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)"
    


【问题讨论】:

您说“我尝试记录退出并再次输入的时间,减去两者并将其添加到运行计数中......当我在手机上运行它时它不起作用。”您发布的代码没有这样做。它使用计时器来增加计数。不要那样做。开始计时器时,将当前 (date()) 保存到 UserDefaults。再次输入函数时,从当前日期中减去保存的日期。这将是自计时器启动以来的秒数,无论您的计时器方法是否一直被调用。 【参考方案1】:

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

我相信您正在寻找的回调函数是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 文件中设置实体和属性:

希望对你有帮助

【讨论】:

我添加了该代码,效果很好。我将我在参考代码中的 exitTime 和 resumeTime 内容添加到您编写的函数中。使用模拟器,即使设备进入睡眠状态,它也能完美运行,但当我在手机上运行它时,它又无法正常工作。是否有特殊原因或需要打开某些权限? 当你说它不起作用时——它是崩溃、不调用方法/做任何事情,还是没有正确更新计时器? - 你的项目部署目标操作系统是什么? - 你的物理设备操作系统呢? 我将代码更新为在 xCode 11.3 和运行 iOS 13.2 的物理设备上测试的完整工作示例 vc 它只是不会更新时间。基本上,如果我在 10 秒时关闭应用程序并在一分钟后打开它,它将从 10 秒恢复。但是,更新后的代码起到了作用,非常感谢您的帮助! 没问题很高兴我能帮上忙!在代码中,您可以看到我们必须存储的两个重要内容是已计时的时间量('alreadyTimed' - 您指的是 10 秒)和应用程序关闭的日期('appCloseTime')-> 然后当它重新打开时,我们计算上次打开和现在之间的差异,并将其添加到“alreadyTimed”的数量中以获取计时器的当前时间。 CoreData 还允许它持久化应用程序终止

以上是关于在手机休眠时运行计时器的主要内容,如果未能解决你的问题,请参考以下文章

手机休眠,js倒计时停止

ios系统屏幕休眠或后台运行倒计时暂停问题

什么是pic单片机定时器1同步模式

JS 倒计时问题,手机网页后台运行时,js会暂停

Android定时的基本实现

Linux设备驱动程序 之 内核定时器