SwiftUI:ObservableObject 不会在重绘时保留其状态

SwiftUI: ObservableObject does not persist its State over being redrawn

问题

为了实现应用代码的简洁外观和感觉,我为每个包含逻辑的视图创建了 ViewModel。

普通的 ViewModel 看起来有点像这样:

class SomeViewModel: ObservableObject {

    @Published var state = 1

    // Logic and calls of Business Logic goes here
}

并像这样使用:

struct SomeView: View {

    @ObservedObject var viewModel = SomeViewModel()

    var body: some View {
        // Code to read and write the State goes here
    }
}

当 Views Parent 没有被更新时,这工作正常。如果父级的状态发生变化,则此视图将被重绘(在声明性框架中很正常)。 但是 ViewModel 也被重新创建并且之后不保持状态。与其他框架(例如:Flutter)相比,这是不寻常的。

在我看来,ViewModel 应该保留,或者 State 应该保留。

如果我将 ViewModel 替换为 @State 属性 并直接使用 int(在此示例中),它会保持不变并且 不会重新创建:

struct SomeView: View {

    @State var state = 1

    var body: some View {
        // Code to read and write the State goes here
    }
}

这显然不适用于更复杂的状态。如果我为 @State 设置一个 class(如 ViewModel),越来越多的东西不会按预期工作。

问题

我知道通常情况下,在内部视图中创建 ViewModel 是不好的做法,但可以使用 NavigationLink 或 Sheet.
复制此行为 有时,当您想到一个非常复杂的 TableView,其中 Cells 本身包含很多逻辑时,将 State 保留在 ParentsViewModel 中并使用绑定是没有用的。
对于个别情况总是有解决方法,但我认为如果不重新创建 ViewModel 会更容易。

重复问题

我知道有很多关于这个问题的问题,都在谈论非常具体的用例。在这里我想谈谈普遍的问题,而不是太深入自定义解决方案。

编辑(添加更详细的示例)

当有一个状态改变的父视图时,比如来自数据库的列表,API,或缓存(想一些简单的事情)。通过 NavigationLink 您可能会到达一个详细信息页面,您可以在其中修改数据。通过更改数据,reactive/declarative 模式会告诉我们还要更新 ListView,然后会 "redraw" NavigationLink,然后会导致重新创建 ViewModel。

我知道我可以将 ViewModel 存储在 ParentView / ParentView 的 ViewModel 中,但在我看来这是错误的做法。由于订阅被销毁 and/or 重新创建 - 可能会有一些副作用。

Is there a way of not recreating the ViewModel every time?

是的,将 ViewModel 实例 放在 SomeView 之外,并通过构造函数注入

struct SomeView: View {
    @ObservedObject var viewModel: SomeViewModel  // << only declaration

Is there a way of replicating the @State Propertywrapper for @ObservedObject?

不需要。 @ObservedObject 已经是 DynamicProperty 类似于 @State

Why is @State keeping the State over the redraw?

因为它保留了它的存储空间,即。包装值,在视图 之外。 (所以,再看上面的第一个)

您需要在 ObservableObject class 中提供自定义 PassThroughSubject。看这段代码:

//
//  Created by Франчук Андрей on 08.05.2020.
//  Copyright © 2020 Франчук Андрей. All rights reserved.
//

import SwiftUI
import Combine


struct TextChanger{
    var textChanged = PassthroughSubject<String,Never>()
    public func changeText(newValue: String){
        textChanged.send(newValue)
    }
}

class ComplexState: ObservableObject{
    var objectWillChange = ObservableObjectPublisher()
    let textChangeListener = TextChanger()
    var text: String = ""
    {
        willSet{
            objectWillChange.send()
            self.textChangeListener.changeText(newValue: newValue)
        }
    }
}

struct CustomState: View {
    @State private var text: String = ""
    let textChangeListener: TextChanger
    init(textChangeListener: TextChanger){
        self.textChangeListener = textChangeListener
        print("did init")
    }
    var body: some View {
        Text(text)
            .onReceive(textChangeListener.textChanged){newValue in
                self.text = newValue
            }
    }
}
struct CustomStateContainer: View {
    //@ObservedObject var state = ComplexState()
    var state = ComplexState()
    var body: some View {
        VStack{
            HStack{
                Text("custom state View: ")
                CustomState(textChangeListener: state.textChangeListener)
            }
            HStack{
                Text("ordinary Text View: ")
                Text(state.text)
            }
            HStack{
                Text("text input: ")
                TextInput().environmentObject(state)
            }
        }
    }
}

struct TextInput: View {
    @EnvironmentObject var state: ComplexState
    var body: some View {
        TextField("input", text: $state.text)
    }
}

struct CustomState_Previews: PreviewProvider {
    static var previews: some View {
        return CustomStateContainer()
    }
}

首先,我使用 TextChanger.text 的新值传递给 CustomState 视图中的 .onReceive(...)。请注意,在这种情况下 onReceive 得到 PassthroughSubject,而不是 ObservableObjectPublisher。在最后一种情况下,您将只有 Publisher.Output in perform: closure,而不是 NewValue。 state.text 在那种情况下会有旧值。

其次,看ComplexStateclass。我做了一个 objectWillChange 属性 来让文本更改手动向订阅者发送通知。它几乎和 @Published wrapper 一样。但是,当文本更改时,它会同时发送 objectWillChange.send()textChanged.send(newValue)。这使您能够准确选择 View,如何对状态变化做出反应。如果您想要普通行为,只需将状态放入 CustomStateContainer 视图中的 @ObservedObject 包装器。然后,您将重新创建所有视图,并且此部分也将获得更新的值:

HStack{
     Text("ordinary Text View: ")
     Text(state.text)
}

如果您不想重新创建所有这些,只需删除@ObservedObject。普通文本View会停止更新,但CustomState会。没有重新创建。

更新: 如果你想要更多的控制,你可以在改变价值的同时决定,你想把这个改变通知给谁。 检查更复杂的代码:

//
//
//  Created by Франчук Андрей on 08.05.2020.
//  Copyright © 2020 Франчук Андрей. All rights reserved.
//

import SwiftUI
import Combine


struct TextChanger{
//    var objectWillChange: ObservableObjectPublisher
   // @Published
    var textChanged = PassthroughSubject<String,Never>()
    public func changeText(newValue: String){
        textChanged.send(newValue)
    }
}

class ComplexState: ObservableObject{
    var onlyPassthroughSend = false
    var objectWillChange = ObservableObjectPublisher()
    let textChangeListener = TextChanger()
    var text: String = ""
    {
        willSet{
            if !onlyPassthroughSend{
                objectWillChange.send()
            }
            self.textChangeListener.changeText(newValue: newValue)
        }
    }
}

struct CustomState: View {
    @State private var text: String = ""
    let textChangeListener: TextChanger
    init(textChangeListener: TextChanger){
        self.textChangeListener = textChangeListener
        print("did init")
    }
    var body: some View {
        Text(text)
            .onReceive(textChangeListener.textChanged){newValue in
                self.text = newValue
            }
    }
}
struct CustomStateContainer: View {
    //var state = ComplexState()
    @ObservedObject var state = ComplexState()
    var body: some View {
        VStack{
            HStack{
                Text("custom state View: ")
                CustomState(textChangeListener: state.textChangeListener)
            }
            HStack{
                Text("ordinary Text View: ")
                Text(state.text)
            }
            HStack{
                Text("text input with full state update: ")
                TextInput().environmentObject(state)
            }
            HStack{
                Text("text input with no full state update: ")
                TextInputNoUpdate().environmentObject(state)
            }
        }
    }
}

struct TextInputNoUpdate: View {
    @EnvironmentObject var state: ComplexState
    var body: some View {
        TextField("input", text: Binding(   get: {self.state.text},
                                            set: {newValue in
                                                self.state.onlyPassthroughSend.toggle()
                                                self.state.text = newValue
                                                self.state.onlyPassthroughSend.toggle()
        }
        ))
    }
}

struct TextInput: View {
    @State private var text: String = ""
    @EnvironmentObject var state: ComplexState
    var body: some View {

        TextField("input", text: Binding(
            get: {self.text},
            set: {newValue in
                self.state.text = newValue
               // self.text = newValue
            }
        ))
            .onAppear(){
                self.text = self.state.text
            }.onReceive(state.textChangeListener.textChanged){newValue in
                self.text = newValue
            }
    }
}

struct CustomState_Previews: PreviewProvider {
    static var previews: some View {
        return CustomStateContainer()
    }
}

我做了一个手动绑定来停止广播 objectWillChange。但是您仍然需要在更改此值的所有地方获取新值以保持同步。这就是为什么我也修改了 TextInput。

这是你需要的吗?

我同意你的看法,我认为这是 SwiftUI 的许多主要问题之一。这是我发现自己在做的事情,虽然很恶心。

struct MyView: View {
  @State var viewModel = MyViewModel()

  var body : some View {
    MyViewImpl(viewModel: viewModel)
  }
}

fileprivate MyViewImpl : View {
  @ObservedObject var viewModel : MyViewModel

  var body : some View {
    ...
  }
}

您可以就地构建视图模型或将其传入,它会为您提供一个视图,该视图将在重建过程中维护您的 ObservableObject。

最后Apple提供了一个解决方案:@StateObject

通过将 @ObservedObject 替换为 @StateObject,我最初 post 中提到的所有内容都有效。

不幸的是,这仅适用于 ios 14+。

这是我在 Xcode 12 Beta(2020 年 6 月 23 日发布)

中的代码
struct ContentView: View {

    @State var title = 0

    var body: some View {
        NavigationView {
            VStack {
                Button("Test") {
                    self.title = Int.random(in: 0...1000)
                }

                TestView1()

                TestView2()
            }
            .navigationTitle("\(self.title)")
        }
    }
}

struct TestView1: View {

    @ObservedObject var model = ViewModel()

    var body: some View {
        VStack {
            Button("Test1: \(self.model.title)") {
                self.model.title += 1
            }
        }
    }
}

class ViewModel: ObservableObject {

    @Published var title = 0
}

struct TestView2: View {

    @StateObject var model = ViewModel()

    var body: some View {
        VStack {
            Button("StateObject: \(self.model.title)") {
                self.model.title += 1
            }
        }
    }
}

如您所见,StateObject 在重绘父视图时保持其值,而 ObservedObject 正在重置。

我的解决方案是使用 EnvironmentObject 而不要在视图中使用 ObservedObject,它的 viewModel 将被重置,你通过层次结构

.environmentObject(viewModel)

只需在某个地方初始化 viewModel,它不会被重置(例如根视图)。