Home > database >  Why does Timer work without @State in SwiftUI?
Why does Timer work without @State in SwiftUI?

Time:10-05

According to multiple sources, including HackingWithSwift, the correct way of using a Timer with SwiftUI is:

struct ContentView: View {
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    @State private var counter = 0

    var body: some View {
        Text("Hello, World!")
            .onReceive(timer) { time in
                if counter == 5 {
                    timer.upstream.connect().cancel()
                } else {
                    print("The time is now \(time)")
                }

                counter  = 1
            }
    }
}

However it seems to me that this would cause a new publisher to be created every time the view is re-rendered. Am I wrong, and if so, why? I would expect that for a persisting object like timer, I would need to use @State. Why don't we need to use @State in this case? To clarify, I would expect the following code to be needed instead:

// Not recommended, but why?
@State var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

It also seems like without @State, the timer would be reset and restarted the next time the view is re-rendered after the timer is canceled like so:

timer.upstream.connect().cancel()

Again, why is this not a problem?

CodePudding user response:

As a mental model it's helpful to think of SwiftUI using two distinct phases to create what it eventually renders [0].

In the first phase it builds the skeleton structure of what a View could render without injecting the runtime data [1]

Then in the second it takes that original structure and injects the initial runtime data and commences runtime rendering and interaction.

In terms of SwiftUI View's, variables without a @ are only being used during construction of the skeleton structure. While the variables with a @ are runtime placeholders as well.

So, in terms of the questions ...

The new timer publisher gets created everytime the top-level ContentView gets evaluated to build the structure for its displayed parent window.

On iOS the code results in a single timer. While on macOS we get a new timer instance for every window we created.

There are no new instance of timer and it doesn't get reset between renderings because SwiftUI's doesn't reevaluate the skeleton structure and trash the existing structure unless it has to.

[0] If familiar with things like PHP, React; it's a bit like them.

CodePudding user response:

The reason this mistake is all over the internet is when SwiftUI first came out the ContentView was init once because it was done in UIKit and passed to a UIHostingController as the root view. Also the advantages of tighter invalidation by breaking up the code into multiple Views wasn't well understood so a lot of early SwiftUI code was all in one ContentView. Nowadays, if the timer is going to be in a View that is init more than once by a parent View's body, then you are correct that it should be @State because as you have identified, it will be restarted everytime the View is re-init which could cause it to fire irregularly.

FYI there is actually a simpler timer as part of date formatting but even today that is still not well understood.

  • Related