It complains that num is used before being initialized, but I can't see why.
If I remove the cancellable variable or change the line self.num = num into _num = State(wrappedValue: num), I can get passed.
Apple guidelines require you to cancel timer when your app move to background and you then need to restart it when your app move back to foreground. For this reason, I avoid using Timer and instead use TimelineView to do periodic things, with it you can make your count down timer view without needing @State.
Here is a one I come up with, I maybe off by one or not
Edit: yes, I did have off by 1 error, I fixed it:
import SwiftUI
struct CountDownSchedule: Sequence, IteratorProtocol {
var count: Int
var deadline = Date()
mutating func next() -> Date? {
if count >= 0 {
defer {
count -= 1
deadline.addTimeInterval(1) // by every second
}
return deadline
} else {
return nil
}
}
}
// need to do `count + 1` because the very first TimelineView event is immediate!
struct CountDownTimerView: View {
// count down this many second
let count: Int
let end: TimeInterval
init(count: Int) {
self.count = count
self.end = Date(timeIntervalSinceReferenceDate: Date().timeIntervalSinceReferenceDate + TimeInterval(count + 1)).timeIntervalSinceReferenceDate
}
static let font = Font.system(size: 50)
var counterView: some View {
TimelineView(.explicit(CountDownSchedule(count: count + 1))) { context in
let now = context.date.timeIntervalSinceReferenceDate
let tick = Int(end - now)
if tick > 0 {
Text("\(tick)")
.font(Self.font)
.foregroundColor(.green)
} else {
Text("Times Up")
.font(Self.font)
.padding(.horizontal)
.foregroundColor(.red)
.background {
Capsule()
.fill(.yellow)
}
}
}
}
var body: some View {
VStack {
Text("Count Down From: \(count)")
counterView
}
}
}
struct ContentView: View {
var body: some View {
VStack {
CountDownTimerView(count: 0)
CountDownTimerView(count: 5)
CountDownTimerView(count: 10)
CountDownTimerView(count: 20)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It’s very easy to tie yourself in knots if you run timers while your iOS app is eligible for suspension. Timers are based off Mach absolute time, which stops when the CPU stops. Consider a scenario like this:
You start a long-running timer.
The user screen locks the device.
The system moves your app to the background.
Nothing requires background execution, so the system stops the CPU.
After a significant delay, the user unlocks the device.
And then brings your app to the front.
At some point in the future the timer will fire. However, it won’t fire at the time you expected because the CPU stopped counting time between steps 4 and 5.