SwiftUI:@StateObject init多次

问题描述 投票:0回答:3

我正在尝试优化我的 SwiftUI 应用程序。我有一个奇怪的行为,ViewModel 在其视图中存储为

@StateObject
。为了理解这个问题,我做了一个小项目来重现它。

ContentView
包含一个用于在工作表中打开
ChildView
的按钮。
ChildView
存储为属性,因为我不想在用户每次打开工作表时重新创建它(这有效):

struct ContentView: View {
   
    @State private var displayingChildView = false
    private let childView = ChildView()
 
    var body: some View {
        Button(action: {
            displayingChildView.toggle()
        }, label: {
            Text("Display child view")
        })
        
        .sheet(isPresented: $displayingChildView, content: {
            childView // instead of: ChildView()
        })
    }
}

ChildView
代码:

struct ChildView: View {
    
    @StateObject private var viewModel = ViewModel()
    
    init() {
        print("init() of ChildView")
    }
    
    var body: some View {
        VStack {
            Button(action: {
                viewModel.add()
            }, label: {
                Text("Add 1 to count")
            })
            
            Text("Count: \(viewModel.count)")
        }
    }
}

还有它的

ViewModel

class ViewModel: ObservableObject {
    @Published private(set) var count = 0
    
    init() {
        print("init() of ViewModel")
    }
    
    func add() {
        count += 1
    }
}

问题来了:

每次用户打开工作表时都会调用 ViewModel 的

init
。为什么?

由于

ViewModel
@StateObject
中的
ChildView
并且
ChildView
仅 init 一次,我预计
ViewModel
也仅 init 一次。

我已阅读这篇文章,上面写着:

用 @StateObject 属性包装器标记的观察对象在其包含的视图结构重绘时不会被销毁和重新实例化。

这里

为您使用的每个可观察对象使用一次@StateObject,无论代码的哪一部分负责创建它。

所以我明白

ViewModel
应该保持活着,特别是因为
ChildView
没有被破坏。

最让我困惑的是,如果我用

@StateObject
替换
@ObservedObject
,它就会按预期工作。但不建议将
@ObservedObject
存储在 View 中。

任何人都可以解释为什么会出现这种行为以及如何按预期修复它(

ViewModel
init 应该被调用一次)?

可能的解决方案:

我找到了解决此问题的可能解决方案:

a.将

ViewModel
的声明移至
ContentView
:

@StateObject private var viewModel = ViewModel()

b.将

ViewModel
中的
ChildView
声明更改为
EnvironmentObject
:

@EnvironmentObject private var viewModel: ViewModel

c.然后注入

childView
:

childView
   .environmentObject(viewModel)

这意味着是

ContentView
负责保持 ChildView 的 ViewModel 处于活动状态。它有效,但我发现这个解决方案非常丑陋:

  • ChildView
    的所有未来子视图都可以通过环境对象访问
    ViewModel
    。但这没有意义,因为它应该只对其视图有用。
  • 我更愿意在其 View 中声明 ViewModel,而不是在其父 View 中。

这个解决方案仍然没有解释上述关于

@StateObject
的问题,应该保持 alive...

swift swiftui
3个回答
1
投票

当视图插入视图层次结构时,SwiftUI 会初始化 @State 变量。这就是为什么您尝试通过将子视图分配给 var 来保持其状态处于活动状态失败的原因。每次显示工作表时,子视图都会添加到视图层次结构中,并初始化其状态变量。

正确的做法是将 viewModel 传递给子视图。

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()
    @State private var displayingChildView = false
    
    var body: some View {
        Button(action: {
            displayingChildView.toggle()
        }, label: {
            Text("Display child view")
        })
        
        .sheet(isPresented: $displayingChildView, content: {
            ChildView(viewModel: viewModel)
        })
    }
}

struct ChildView: View {
    @ObservedObject var viewModel: ViewModel
    
    var body: some View {
        VStack {
            Button(action: {
                viewModel.add()
            }, label: {
                Text("Add 1 to count")
            })
            
            Text("Count: \(viewModel.count)")
        }
    }
}

0
投票

如果你使用 observableobject 那么他的工作就是观察一个变量或类。因此,如果视图中有任何操作执行,并且您使用 observableobject 类的 stateobject 或observedobject 显示操作执行,则调用 obsevabobject 类。并在调用类时初始化调用。


-1
投票

对象会减慢 SwiftUI 的速度,为了有效地使用它,我们需要忘记视图模型对象并学习值类型、结构、变异函数、闭包捕获等。这是应该如何完成的:

struct Counter {
    private(set) var count = 0
    
    init() {
        print("init() of Counter")
    }
    
    mutating func add() {
        count += 1
    }
}

struct ChildView: View {
    
    @State private var counter = Counter()
    
    init() {
        print("init() of ChildView")
    }
    
    var body: some View {
        VStack {
            Button(action: {
                counter.add()
            }, label: {
                Text("Add 1 to count")
            })
            
            Text("Count: \(counter.count)")
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.