简体   繁体   中英

Why does SwiftUI @State performs differently than @ObservedObject for simple state changes?

If I use @State to track selectedTab for TabView then the View 's of the Tab will NOT get invalidated (their init is NOT called).

However, if I use @ObservedObject to track selectedTab for TabView then the View 's of the Tab WILL get invalidated (their init IS called`).

Why? I'm simply moving from @State to @ObservedObject yet its doing different things.

Below is code that can be plugged into a Playground. Switch between TabView initializers to see the difference. If we use @State then the "Init called for OneView!" will be printed once. If we use @ObservedObject then "Init called for OneView!" will be called on every tab switch.

import SwiftUI
import PlaygroundSupport

enum Tab: String {
  case one
  case two
}

struct ContentView: View {

  @State var selectedTab: Tab = .one
  @ObservedObject var selectedTabObject: SelectedTabAsObject

  init(selectedTabObject: SelectedTabAsObject) {
    self.selectedTabObject = selectedTabObject
    print("Init called for ContentView!")
  }

  var body: some View {
// TODO: Switch between commenting out the bottom 2 lines
    TabView(selection: $selectedTab) {
//    TabView(selection: $selectedTabObject.selectedTab) {
      OneView()
        .tabItem {
          Text("One")
        }
        .tag(Tab.one)
      TwoView()
        .tabItem {
          Text("Two")
        }
        .tag(Tab.two)
    }
  }
}

class SelectedTabAsObject: ObservableObject {
  @Published var selectedTab: Tab = .one

  init() {
    print("Init called for SelectedTabAsObject!")
  }
}

struct OneView: View {

  init() {
    print("Init called for OneView!")
  }

  var body: some View {
    Text("One View")
  }
}

struct TwoView: View {
  var body: some View {
    Text("Two View")
  }
}


struct AppView: View {

  let selectedTabObject: SelectedTabAsObject

  init() {
    self.selectedTabObject = SelectedTabAsObject()
    print("Init called for AppView!")
  }

  var body: some View {
    ContentView(selectedTabObject: selectedTabObject)
  }
}

PlaygroundPage.current.setLiveView(AppView())

After I posted this, I think I know the reason (although I don't really fully understand it).

@State is keeping a track of a simple value (enum/ String ).

@ObservedObject is an object.

When SwiftUI asks: "Should I reload body?" it needs to know what changed. In the case of @State it knows that only the value changed. In the case of @ObservedObject , it's not sure so it reloads the whole body .

The fix would likely be to implement Equatable for the View to avoid unnecessary body reloads. However, the Equatable method doesn't get called even when marking ContentView as .equatable . There might be some SwiftUI bugs.

The following post has some notes about this:

Here is a post on Equatable and .equatable() :

Interesting discussion about how SwiftUI does comparisons:

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM