就像@Mark指出的那样,您不应该在初始化期间处理@StateObject
。这是因为@StateObject
在View.init()之后略微在body被调用之前/之后初始化。
我尝试了许多不同的方法来从一个视图传递数据到另一个视图,并提出了一种适用于简单和复杂视图/视图模型的解决方案。
版本
Apple Swift version 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8)
这个解决方案可以在iOS14.0及以上版本中使用,因为你需要使用 .onChange()
视图修饰符。这个例子是用Swift Playgrounds编写的。如果您需要一个类似于 onChange
的修饰符来支持较低版本的iOS系统,则需要自己编写一个修饰符。
主视图
主视图包含一个@StateObject viewModel
,处理所有视图逻辑,例如按钮点击和“数据”(testingID: String)
->请查看ViewModel。
struct TestMainView: View {
@StateObject var viewModel: ViewModel = .init()
var body: some View {
VStack {
Button(action: { self.viewModel.didTapButton() }) {
Text("TAP")
}
Spacer()
SubView(text: $viewModel.testingID)
}.frame(width: 300, height: 400)
}
}
主视图模型 (ViewModel)
视图模型发布一个 testID: String?
。这个testID可以是任何类型的对象(例如配置对象等),对于本例子,它只是一个在子视图中需要使用的字符串。
final class ViewModel: ObservableObject {
@Published var testingID: String?
func didTapButton() {
self.testingID = UUID().uuidString
}
}
因此,通过点击按钮,我们的
ViewModel
将更新
testID
。我们还希望在我们的
SubView
中使用此
testID
,如果它发生更改,我们也希望我们的
SubView
能够识别并处理这些更改。通过
ViewModel @Published var testingID
,我们能够向我们的视图
发布更改。现在让我们来看一下我们的
SubView和
SubViewModel。
SubView
因此,
SubView
具有自己的
@StateObject
来处理其自己的逻辑。它与其他视图和ViewModel完全分开。在此示例中,
SubView
仅显示来自其
MainView
的
testID
。但请记住,它可以是任何类型的对象,例如数据库请求的预设和配置。
struct SubView: View {
@StateObject var viewModel: SubviewModel = .init()
@Binding var test: String?
init(text: Binding<String?>) {
self._test = text
}
var body: some View {
Text(self.viewModel.subViewText ?? "no text")
.onChange(of: self.test) { (text) in
self.viewModel.updateText(text: text)
}
.onAppear(perform: { self.viewModel.updateText(text: test) })
}
}
为了“连接”我们的
MainViewModel
发布的
testingID
,我们用
@Binding
初始化我们的
SubView
。现在我们在
SubView
中有相同的
testingID
。但是我们不想直接在视图中使用它,而是需要将数据传递到我们的
SubViewModel
中,记住我们的
SubViewModel
是一个
@StateObject
,用于处理所有的逻辑。并且我们不能在视图初始化期间将值传递给
@StateObject
,就像一开始我写的那样。此外,如果我们
MainViewModel
中的数据 (
testingID: String
) 发生更改,我们的
SubViewModel
应该识别和处理这些更改。
因此,我们使用两个ViewModifiers
。
onChange
.onChange(of: self.test) { (text) in
self.viewModel.updateText(text: text)
}
onChange修改器订阅我们@Binding
属性的更改。因此,如果它发生了更改,这些更改将传递给我们的SubViewModel
。请注意,您的属性需要是Equatable的。如果您传递了一个更复杂的对象,比如一个Struct
,请确保在您的Struct
中实现此协议。
onAppear
我们需要onAppear
来处理“第一次初始数据”,因为onChange不会在视图第一次初始化时触发。它仅用于更改。
.onAppear(perform: { self.viewModel.updateText(text: test) })
好的,这里是SubViewModel,我想没有更多需要解释的了。
class SubviewModel: ObservableObject {
@Published var subViewText: String?
func updateText(text: String?) {
self.subViewText = text
}
}
现在,您的数据同步在MainViewModel和SubViewModel之间。这种方法适用于具有许多子视图及其子视图等的大型视图。它还可以使您的视图和相应的viewModel具有高重用性。
工作示例
Github上的Playground:
https://github.com/luca251117/PassingDataBetweenViewModels
附加说明
为什么我使用onAppear
和onChange
而不是只有onReceive
: 似乎用这两个修饰符替换onReceive
会导致连续的数据流多次触发SubViewModel updateText
。如果需要将数据流传递给表示层,这可能是可以接受的,但如果要处理网络调用等操作,则可能会导致问题。这就是为什么我更喜欢“两个修饰符法”的原因。
个人建议:请不要在对应视图范围之外修改stateObject。即使某种方式可能是可行的,但这并不是其设计初衷。