SwiftUI init()中@StateObject初始化异常问题原因及解决方案
问题原因
@StateObject的实例生命周期完全由SwiftUI框架托管,和View结构体自身的属性初始化流程是分离的:
- 在View的
init()方法里直接访问objects属性时,拿到的只是SwiftUI临时生成的Objects实例,所有添加节点、修改show属性的操作都只作用在这个临时实例上。 - 你控制台中两次调用
addNode都打印count为1,本质是init阶段每次访问objects,SwiftUI都可能生成新的临时实例,两次add操作分别作用在两个完全无关的临时对象上;最后打印toString时访问到的又是第三个全新的空实例,自然输出nodes count为0。 - 等View正式被加入渲染树,SwiftUI会自行创建并持久持有一个新的
Objects实例作为正式状态对象,你之前修改的所有临时实例都会被直接丢弃,最终UI加载完成后读取到的nodes数组必然是空的。 - 你换成
@State声明时看起来逻辑正常,属于偶然现象:@State本身是为值类型设计的,用它持有ObservableObject这类引用类型时,只是刚好当前场景下临时实例没有被SwiftUI替换。一旦出现视图重建、跨视图传值的场景,很容易出现状态丢失、状态不同步的问题,完全无法满足跨视图共享实例的业务要求。
可行解决方案
根据你的业务场景,三选一即可:
方案1:把初始化逻辑移到ObservableObject的init方法中
这是最符合SwiftUI设计规范的写法,适合初始化逻辑固定、不需要依赖View入参的场景。
class Objects: ObservableObject { @Published var nodes: [Node] = [] @Published var show: Bool = false init() { // 把原有View init里的初始化逻辑移到这里 let node1 = Node() node1.x = 50 node1.y = 200 let node2 = Node() node2.x = 200 node2.y = 100 self.nodes = [node1, node2] self.show = true } // 原有addNode、toString方法保持不变 func addNode(_ node: Node) { self.nodes.append(node) print(self.nodes.count) } func toString() -> String { var s = "Nodes: (\(self.nodes.count))\n" for node in self.nodes { s += node.toString() + "\n" } return s } } struct ContentView: View { // 正常声明即可,SwiftUI创建实例时会自动执行Objects的init完成初始化 @StateObject var objects = Objects() // 不需要再写自定义init,原有init逻辑全部删除 var body: some View { Button("Add") { let i = objects.nodes.count let node = Node() node.x = Double(i) node.y = Double(i) objects.addNode(node) print(objects.toString()) } .padding() .foregroundColor(.white) .background(.red) } }
方案2:在View的init中给StateObject传入预先构造好的实例
适合初始化逻辑需要依赖View初始化入参的动态场景,注意必须对带下划线的属性投影(也就是StateObject的存储本体)赋值,直接修改objects属性无效。
struct ContentView: View { @StateObject var objects: Objects init(customPositionX: Double = 50, customPositionY: Double = 200) { // 先构造好配置完成的Objects实例 let initObject = Objects() let node1 = Node() node1.x = customPositionX node1.y = customPositionY initObject.addNode(node1) let node2 = Node() node2.x = 200 node2.y = 100 initObject.addNode(node2) initObject.show.toggle() // 把构造完成的实例传给StateObject,这个实例会被SwiftUI作为持久化状态持有 _objects = StateObject(wrappedValue: initObject) } // body部分保持不变 var body: some View { Button("Add") { let i = objects.nodes.count let node = Node() node.x = Double(i) node.y = Double(i) objects.addNode(node) print(objects.toString()) } .padding() .foregroundColor(.white) .background(.red) } }
方案3:在视图首次加载的生命周期回调中做初始化
适合初始化逻辑需要用到其他视图状态、或者需要执行异步操作的场景,通过标记位保证逻辑只执行一次。
struct ContentView: View { @StateObject var objects = Objects() // 标记初始化是否已经执行过,避免onAppear重复触发 @State private var hasCompletedInit = false var body: some View { Button("Add") { let i = objects.nodes.count let node = Node() node.x = Double(i) node.y = Double(i) objects.addNode(node) print(objects.toString()) } .padding() .foregroundColor(.white) .background(.red) .onAppear { guard !hasCompletedInit else { return } let node1 = Node() node1.x = 50 node1.y = 200 objects.addNode(node1) let node2 = Node() node2.x = 200 node2.y = 100 objects.addNode(node2) objects.show.toggle() hasCompletedInit = true } } }
注意:以上三种方案初始化的Objects实例都会被SwiftUI正确持有,支持通过
.environmentObject或者参数传递给子视图共享状态,不会出现实例被重复创建、数据丢失的问题。
内容的提问来源于stack exchange,提问作者Antonio
相关产品推荐
相关产品推荐

