SwiftUI视图自动旋转时重复初始化问题的解决方案咨询
问题代码
struct TestContentView: View { @Environment(\.verticalSizeClass) var verticalSizeClass @Environment(\.horizontalSizeClass) var horizontalSizeClass var body: some View { TestView() .ignoresSafeArea() } } struct TestView: View { @State var model:TestModel = TestModel() var body: some View { Text("Count \(model.counter)") } } @Observable class TestModel { var counter:Int = 0 deinit { print("Calling deinit on TestModel") } }
问题描述
尽管TestModel通过@State变量持有,但设备自动旋转时,TestView和TestModel仍会被重新初始化,不符合预期。已知问题根源是父视图TestContentView声明了size class环境变量,导致TestView被重置。将TestModel移到父视图初始化是一种解决方法,但对于较深的视图层级来说,扩展性不佳。请问针对该问题的正确解决方法是什么,尤其当视图层级较深且上层包含horizontalSizeClass或verticalSizeClass时?
解决方案
1. 用@StateObject替代@State管理Observable类
你的TestModel是@Observable标记的引用类型,应该使用@StateObject而非@State来持有。@State专为值类型设计,而@StateObject负责管理引用类型的生命周期,确保视图重建时对象不会被销毁重建。
修改TestView:
struct TestView: View { @StateObject var model: TestModel = TestModel() // 替换@State为@StateObject var body: some View { Text("Count \(model.counter)") } }
这样即使父视图因size class变化重建,TestView中的model实例会被保留,不会触发deinit。
2. 隔离size class读取逻辑,避免父视图重建传递到子视图
如果必须在父视图中使用size class,可以通过onChange监听变化,而非直接在结构体顶部声明环境变量(直接声明会触发父视图重建,进而导致子视图重置)。
修改TestContentView:
struct TestContentView: View { var body: some View { TestView() .ignoresSafeArea() .onChange(of: [ EnvironmentValues.verticalSizeClass, EnvironmentValues.horizontalSizeClass ]) { newValue in // 在这里处理size class变化的业务逻辑 print("Size class changed: \(newValue)") } } }
如果需要根据size class调整布局,也可以用@Environment结合Group或ViewBuilder,让SwiftUI尝试复用子视图实例:
struct TestContentView: View { @Environment(\.verticalSizeClass) var verticalSizeClass var body: some View { Group { if verticalSizeClass == .compact { TestView() } else { TestView() } } .ignoresSafeArea() } }
3. 通过环境(Environment)传递模型(适配深层视图层级)
如果视图层级较深,不想逐层传递模型,可以将TestModel注入到环境中,让深层视图直接从环境获取,同时用@StateObject保证模型生命周期不受上层视图重建影响。
首先扩展EnvironmentValues:
extension EnvironmentValues { var testModel: TestModel { get { self[TestModelKey.self] } set { self[TestModelKey.self] = newValue } } } private struct TestModelKey: EnvironmentKey { static let defaultValue: TestModel = TestModel() }
然后在父视图中持有模型并注入环境:
struct TestContentView: View { @Environment(\.verticalSizeClass) var verticalSizeClass @Environment(\.horizontalSizeClass) var horizontalSizeClass @StateObject private var testModel = TestModel() var body: some View { TestView() .ignoresSafeArea() .environment(\.testModel, testModel) } }
最后在TestView中从环境获取模型:
struct TestView: View { @Environment(\.testModel) var model var body: some View { Text("Count \(model.counter)") } }
这种方式下,模型的生命周期由父视图的@StateObject管理,无论上层视图如何重建,模型都不会被销毁,深层视图也能直接访问。
关键原理
@State适用于值类型(如struct、enum),视图重建时SwiftUI会保留其值,但无法保证引用类型的实例不被重新初始化。@StateObject专为Observable引用类型设计,SwiftUI会维护其生命周期,确保视图重建时实例被保留。- 父视图声明
@Environment(\.sizeClass)后,size class变化会触发父视图重建,若子视图未用@StateObject或环境传递模型,就会被重新初始化。
内容的提问来源于stack exchange,提问作者Deepak Sharma

