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?
Then, I added
onAppear
modifier toCountdownView
to detect the recreating of the view. As a result, it is created only once.
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 own CountdownView.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 in body
is to provide a description of the view you are trying to create, and SwiftUI creates new views (this calls onAppear
), or updates existing views (this doesn't call onAppear
), all based on your descriptions. This is unlike in UIKit, where creating a UILabel
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 to TimelineView
. SwiftUI runs this closure at appropriate times, and when it does so, it compares the View
(i.e. the description of what views you want) it returns, with the View
it returned last time.
SwiftUI sees that you used onChange(of: date)
both in the previous description, and in this description, so it compares the date
s from both descriptions, and finds that they are not equal. Therefore, it calls the closure you passed to onChange
.
Of course, how it actually works is much more complicated, but this should be just enough to understand why you can onChange
of a let
constant. SwiftUI is comparing the previous version of your view's body
with the new version's body
.