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.