简体   繁体   English

SwiftUI:如何在后台运行计时器

[英]SwiftUI: How to run a Timer in background

I have built a basic timer app and I'm trying to figure out on how to run the timer in background.我已经构建了一个基本的计时器应用程序,我正在尝试弄清楚如何在后台运行计时器。 I have tried the Background Mode from Signing and Capabilities and doesn't seem to work out for me.我已经尝试了Signing 和 Capabilities 中后台模式,但似乎对我不起作用。

I'm currently working on Xcode 12 beta 6.我目前正在开发 Xcode 12 beta 6。

Code代码

struct ContentView: View {
    @State var start = false
    @State var count = 0
    
    var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    
    var body: some View {
        ZStack {
            // App content.
        }
        .onReceive(timer, perform: { _ in
            if start {
                if count < 15 {
                    count += 1
                } else {
                    start.toggle()
                }
            }
        })
    }
}

If anyone of you have any suggestions on managing the timer in a better way, please do let me know.如果你们中的任何人对以更好的方式管理计时器有任何建议,请告诉我。 Thanks.谢谢。

When user leaves the app, it is suspended.当用户离开应用程序时,它被暂停。 One generally doesn't keep timers going when the user leaves the app.当用户离开应用程序时,通常不会让计时器继续运行。 We don't want to kill the user's battery to update a timer that really isn't relevant until the user returns to the app.我们不想耗尽用户的电池来更新一个真正不相关的计时器,直到用户返回应用程序。

This obviously means that you do not want to use the “counter” pattern.这显然意味着您不想使用“计数器”模式。 Instead, capture the the Date when you started the timer, and save it in case the user leaves the app:相反,在您启动计时器时捕获Date ,并保存它以防用户离开应用程序:

func saveStartTime() {
    if let startTime = startTime {
        UserDefaults.standard.set(startTime, forKey: "startTime")
    } else {
        UserDefaults.standard.removeObject(forKey: "startTime")
    }
}

And, when the app starts, retrieved the saved startTime :并且,当应用程序启动时,检索保存的startTime

func fetchStartTime() -> Date? {
    UserDefaults.standard.object(forKey: "startTime") as? Date
}

And your timer should now not use a counter, but rather calculate the elapsed time between the start time and now:并且您的计时器现在不应该使用计数器,而是计算开始时间和现在之间经过的时间:

let now = Date()
let elapsed = now.timeIntervalSince(startTime)

guard elapsed < 15 else {
    self.stop()
    return
}

self.message = String(format: "%0.1f", elapsed)

Personally, I'd abstract this timer and persistence stuff out of the View :就个人而言,我会从View抽象出这个计时器和持久性的东西:

class Stopwatch: ObservableObject {
    /// String to show in UI
    @Published private(set) var message = "Not running"

    /// Is the timer running?
    @Published private(set) var isRunning = false

    /// Time that we're counting from
    private var startTime: Date?                        { didSet { saveStartTime() } }

    /// The timer
    private var timer: AnyCancellable?

    init() {
        startTime = fetchStartTime()

        if startTime != nil {
            start()
        }
    }
}

// MARK: - Public Interface

extension Stopwatch {
    func start() {
        timer?.cancel()               // cancel timer if any

        if startTime == nil {
            startTime = Date()
        }

        message = ""

        timer = Timer
            .publish(every: 0.1, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] _ in
                guard
                    let self = self,
                    let startTime = self.startTime
                else { return }

                let now = Date()
                let elapsed = now.timeIntervalSince(startTime)

                guard elapsed < 60 else {
                    self.stop()
                    return
                }

                self.message = String(format: "%0.1f", elapsed)
            }

        isRunning = true
    }

    func stop() {
        timer?.cancel()
        timer = nil
        startTime = nil
        isRunning = false
        message = "Not running"
    }
}

// MARK: - Private implementation

private extension Stopwatch {
    func saveStartTime() {
        if let startTime = startTime {
            UserDefaults.standard.set(startTime, forKey: "startTime")
        } else {
            UserDefaults.standard.removeObject(forKey: "startTime")
        }
    }

    func fetchStartTime() -> Date? {
        UserDefaults.standard.object(forKey: "startTime") as? Date
    }
}

Then the view can just use this Stopwatch :然后视图可以只使用这个Stopwatch

struct ContentView: View {
    @ObservedObject var stopwatch = Stopwatch()

    var body: some View {
        VStack {
            Text(stopwatch.message)
            Button(stopwatch.isRunning ? "Stop" : "Start") {
                if stopwatch.isRunning {
                    stopwatch.stop()
                } else {
                    stopwatch.start()
                }
            }
        }
    }
}

FWIW, UserDefaults probably isn't the right place to store this startTime . FWIW, UserDefaults可能不是存储此startTime的正确位置。 I'd probably use a plist or CoreData or whatever.我可能会使用 plist 或 CoreData 或其他任何东西。 But I wanted to keep the above as simple as possible to illustrate the idea of persisting the startTime so that when the app fires up again, you can make it look like the timer was running in the background, even though it wasn't.但我想尽可能简单地说明保持startTime的想法,这样当应用程序再次启动时,你可以让它看起来像计时器在后台运行,即使它不是。

import SwiftUI导入 SwiftUI

struct BackgroundTimer: View {
    @State var start = false
    @State var count = 0
        
    var body: some View {
        VStack {
            Text(count.description)
            Text("Timer isRunning == \(start.description)")
            Button(action: {
                start = true
                count = 0
                Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
                    count += 1
                    
                    if count == 15 {
                        timer.invalidate()
                        start = false
                    }
                }
            }, label: {
                Text("Start Timer")
            })
        }
        
    }
}

struct BackgroundTimer_Previews: PreviewProvider {
    static var previews: some View {
        BackgroundTimer()
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM