SwiftUI 计时器视图暂停屏幕上的所有其他视图

SwiftUI timer view pausing all other views on screen

我很好奇是否有办法让计时器随着 UI 更新(就像现在一样),即使有人正在滚动。此外,我想确保 UI 每秒更新一次,屏幕不会冻结。此问题已根据我之前收到的有用答案进行了更新。

struct ContentView: View {
    var body: some View {
        NavigationView{
            NavigationLink(destination: ScrollTest()){
                HStack {
                     Text("Next Screen")
                }
            }
        }
    }
}
struct ScrollTest: View {
    @ObservedObject var timer = SectionTimer(duration: 60)
    var body: some View {
        HStack{
            List{
                Section(header: TimerNavigationView(timer: timer)){
                    ForEach((1...50).reversed(), id: \.self) {
                        Text("\([=10=])…").onAppear(){
                            self.timer.startTimer()
                        }

                    }
                }
            }.navigationBarItems(trailing: TimerNavigationView(timer: timer))
        }

    }
}
struct TimerNavigationView: View {
    @ObservedObject var timer: SectionTimer
    var body: some View{
        HStack {
            Text("\(timer.timeLeftFormatted) left")
            Spacer()
        }
    }
}
class SectionTimer:ObservableObject {
    private var endDate: Date
    private var timer: Timer?
    var timeRemaining: Double {
        didSet {
            self.setRemaining()
        }
    }
    @Published var timeLeftFormatted = ""

    init(duration: Int) {
        self.timeRemaining = Double(duration)
        self.endDate = Date().advanced(by: Double(duration))
        self.startTimer()

    }

    func startTimer() {
        guard self.timer == nil else {
            return
        }
        self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { (timer) in
            self.timeRemaining = self.endDate.timeIntervalSince(Date())
            if self.timeRemaining < 0 {
                timer.invalidate()
                self.timer = nil
            }
        })
    }

    private func setRemaining() {
        let min = max(floor(self.timeRemaining / 60),0)
        let sec = max(floor((self.timeRemaining - min*60).truncatingRemainder(dividingBy:60)),0)
        self.timeLeftFormatted = "\(Int(min)):\(Int(sec))"
    }

    func endTimer() {
        self.timer?.invalidate()
        self.timer = nil
    }
}

虽然 SwiftUI 有一个计时器,但我认为在这种情况下使用它不是正确的方法。

您的模型应该为您处理时间。

如果您的视图直接观察其模型对象而不是试图观察您的可观察对象的 属性 中的数组成员,这也会有所帮助。

你没有展示你的 SectionTimer,但这是我创建的:

class SectionTimer:ObservableObject {
    private var endDate: Date
    private var timer: Timer?
    var timeRemaining: Double {
        didSet {
            self.setRemaining()
        }
    }

    @Published var timeLeftFormatted = ""

    init(duration: Int) {
        self.timeRemaining = Double(duration)
        self.endDate = Date().advanced(by: Double(duration))
        self.startTimer()

    }

    func startTimer() {
        guard self.timer == nil else {
            return
        }
        self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { (timer) in
            self.timeRemaining = self.endDate.timeIntervalSince(Date())
            if self.timeRemaining < 0 {
                timer.invalidate()
                self.timer = nil
            }
        })
    }

    private func setRemaining() {
        let min = max(floor(self.timeRemaining / 60),0)
        let sec = max(floor((self.timeRemaining - min*60).truncatingRemainder(dividingBy:60)),0)
        self.timeLeftFormatted = "\(Int(min)):\(Int(sec))"
    }

    func endTimer() {
        self.timer?.invalidate()
        self.timer = nil
    }
}

它使用Date而不是从剩余计数器中减去;这更准确,因为计时器不会以精确的时间间隔计时。它更新 timeLeftFormatted 已发布 属性。

为了使用它,我对您的 TimerNavigationView -

进行了以下更改
struct TimerNavigationView: View {
    @ObservedObject var timer: SectionTimer
    var body: some View{
        HStack {
            Text("\(timer.timeLeftFormatted) left")
            Spacer()
        }
    }
}

您可以看到将计时器放入模型中如何极大地简化您的视图。

您可以通过 .navigationBarItems(trailing: TimerNavigationView(timer: self.test.timers[self.test.currentSection]))

使用它

更新

问题中更新的代码有助于证明问题,我在 this answer

中找到了解决方案

当 scrollview 正在滚动时,当前 RunLoop 的模式会发生变化,并且不会触发计时器。

解决方案是自己在 common 模式下安排计时器,而不是依赖 scheduledTimer -

获得的 default 模式
func startTimer() {
    guard self.timer == nil else {
        return
    }

    self.timer = Timer(timeInterval: 0.2, repeats: true) { (timer) in
        self.timeRemaining = self.endDate.timeIntervalSince(Date())
        if self.timeRemaining < 0 {
            timer.invalidate()
            self.timer = nil
        }
    }
    RunLoop.current.add(self.timer!, forMode: .common)
}