When I study the SwiftUI Apprentice book of Kodeco, I come across the below code snippet.
struct CountdownView: View {
let date: Date
@Binding var timeRemaining: Int
let size: Double
var body: some View {
Text("\(timeRemaining)") // 5
.font(.system(size: size, design: .rounded))
.padding()
.onChange(of: date) { _ in // 6
timeRemaining -= 1
}
}
}
struct TimerView: View {
@State private var timeRemaining: Int = 3 // 1
@Binding var timerDone: Bool // 2
let size: Double
var body: some View {
TimelineView( // 3
.animation(
minimumInterval: 1.0,
paused: timeRemaining <= 0)) { context in
CountdownView( // 4
date: context.date,
timeRemaining: $timeRemaining,
size: size)
}
.onChange(of: timeRemaining) { _ in
if timeRemaining < 1 {
timerDone = true // 7
}
}
}
}
struct TimerView_Previews: PreviewProvider {
static var previews: some View {
TimerView(timerDone: .constant(false), size: 90)
}
}
When I examine the code snippet, I firstly expected to be recreated CountdownView
. But there would be no change on date
property in that case. Then, I added onAppear
modifier to CountdownView
to detect the recreating of the view. As a result, it is created only once. I come up with a question to help me to understand the view rending mechanism of SwiftUI, on which I still work that although date
property is a constant and does not use @Binding
how is it possible to observe the change of a data that is declared as a constant in SwiftUI?
While
onAppear
can indeed be used to detect the view being recreated. "Recreated" here does not mean "CountdownView.init
got called". You can handwrite your ownCountdownView.init
to detect this.onAppear
gets called when the actual thing on the screen gets removed and recreated. This happens when, e.g. the identity of the view changes (learn more about identity in Demystifying SwiftUI). Note that all you are doing inbody
is to provide a description of the view you are trying to create, and SwiftUI creates new views (this callsonAppear
), or updates existing views (this doesn't callonAppear
), all based on your descriptions. This is unlike in UIKit, where creating aUILabel
with its initialiser directly creates something that can be shown on the screen.In your code, you are calling
CountdownView.init
in a closure passed toTimelineView
. SwiftUI runs this closure at appropriate times, and when it does so, it compares theView
(i.e. the description of what views you want) it returns, with theView
it returned last time.SwiftUI sees that you used
onChange(of: date)
both in the previous description, and in this description, so it compares thedate
s from both descriptions, and finds that they are not equal. Therefore, it calls the closure you passed toonChange
.Of course, how it actually works is much more complicated, but this should be just enough to understand why you can
onChange
of alet
constant. SwiftUI is comparing the previous version of your view'sbody
with the new version'sbody
.