@State cause compile error in initializer

I am implementing a countdown view with SwiftUI. I got a compile error which confuse me a lot.
Here's my code:

struct LabCountDownView: View {
    @State private var num: Int
    let timer: Timer.TimerPublisher
    var cancellable: Cancellable
    init(num: Int) {
        self.num = num
        timer = Timer.TimerPublisher(interval: 1.0, runLoop: .main, mode: .common)
        cancellable = timer.connect()
    }
    
    var body: some View {
        Text("\(num)")
            .padding()
            .onReceive(timer) { _ in
                num -= 1
                if num == 0 {
                    self.cancellable.cancel()
                }
            }
    }
}

Here's the error:

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.

Does anyone know why it's failed to compile?

Unfortunately SwiftUI questions are inappropriate on this forum. Try stackoverflow.

When I compile your code I don't get any errors. Maybe you're using num outside this code?

self.num = State(initialValue: num)

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()
    }
}
1 Like

Please show the source of that requirement.

When app moves to background timers normally suspend shortly (unless you keep the app running in background somehow).

Search for " Invalidate any active timers." : Preparing your UI to run in the background | Apple Developer Documentation

also:

  • Suspend dispatch and operation queues.
  • Don’t schedule any new tasks for execution.

I wonder how async/await deals with these?

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:

  1. You start a long-running timer.

  2. The user screen locks the device.

  3. The system moves your app to the background.

  4. Nothing requires background execution, so the system stops the CPU.

  5. After a significant delay, the user unlocks the device.

  6. 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.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

4 Likes