为什么SwiftUI中@State与@ObservedObject处理简单状态变化表现不同?
核心差异原因
这个行为差异本质是SwiftUI对@State和@ObservedObject两类状态源的重渲染策略不同导致的:
@State是SwiftUI框架内部托管存储的属性,它的存储位置和当前View的生命周期绑定,不属于View结构体本身的成员值。当@State修饰的selectedTab变化时,SwiftUI只会触发ContentView的body局部增量重计算,重计算过程中会自动对比子View的结构差异,对于没有任何入参变更、标识稳定的OneView和TwoView,SwiftUI会直接复用之前创建的实例,不会重新调用它们的init方法。@ObservedObject对应的是外部存储的可观察对象,当它的@Published属性变更时,会通过objectWillChange发布者通知SwiftUI,触发整个依赖该对象的View树全量重计算。此时SwiftUI会重新构造body内的所有子View实例,哪怕子View没有任何入参变化,也会执行init方法。
可选优化方案
如果需要用@ObservedObject管理选中状态同时避免Tab对应的View重复初始化,可以给子View添加固定的唯一标识,告知SwiftUI该View的标识未发生变化,可以复用实例:
OneView() .tabItem { Text("One") } .tag(Tab.one) .id(Tab.one) // 固定标识
也可以用EquatableView包装子View,让SwiftUI根据相等性判断是否需要重新创建实例。
复现代码
可直接在Playground运行的复现代码如下,切换两个TabView初始化方法的注释即可复现差异:
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: 切换下面两行的注释即可复现差异 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())
内容的提问来源于stack exchange,提问作者kgaidis
相关产品推荐
相关产品推荐

