iosswiftuiswiftui-viewviewrendering

What is the explanation of observing a constant data change in SwiftUI View


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?


Solution

  • Then, I added onAppear modifier to CountdownView 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 dates 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.