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

时间:2020-05-08 10:15:13

标签: swift swiftui declarative property-wrapper

问题

为了获得App代码的清晰外观,我为包含逻辑的每个View创建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,并且此后不保存State。与其他框架(例如Flutter)进行比较时,这是不寻常的。

我认为ViewModel应该保留,或者State应该保留。

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

struct SomeView: View {

    @State var state = 1

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

这显然不适用于更复杂的国家。而且,如果我为@State设置了一个类(例如ViewModel),那么越来越多的事情将无法正常工作。

问题

  • 有没有一种方法不能每次都重新创建ViewModel?
  • 是否可以为@State复制@ObservedObject Propertywrapper?
  • 为什么@State保留重绘的状态?

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

重复问题

我知道有很多关于这个问题的问题,都在讨论非常特定的用例。在这里,我想谈一谈一般性问题,而不必深入探讨自定义解决方案。

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

具有状态更改的ParentView时,例如来自数据库,API或缓存的列表(考虑一些简单的事情)。通过NavigationLink,您可能会到达详细信息页面,您可以在其中修改数据。通过更改数据,反应性/声明性模式将告诉我们也更新ListView,然后“重绘” NavigationLink,然后重新创建ViewModel。

我知道我可以将ViewModel存储在ParentView / ParentView的ViewModel中,但这是IMO的错误方法。而且由于订阅被销毁和/或重新创建-可能会有一些副作用。

4 个答案:

答案 0 :(得分:6)

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

通过将@ObservedObject替换为@StateObject,我在最初的帖子中提到的所有内容都可以正常工作。

不幸的是,这仅在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时,在重绘父视图时保持其值。

答案 1 :(得分:2)

我同意你的看法,我认为这是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。

答案 2 :(得分:0)

有没有一种方法不能每次都重新创建ViewModel?

是的,将ViewModel实例保留在SomeView的外部中,并通过构造函数进行注入

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

是否可以为@ObservedObject复制@State Propertywrapper?

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

为什么@State保留重绘的状态?

因为它保持其存储,即。包装的值,外部视图。 (因此,请再次参见上文)

答案 3 :(得分:0)

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

//
//  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的新值传递给.onReceive(...)视图中的CustomState。请注意,在这种情况下,onReceive得到PassthroughSubject,而不是ObservableObjectPublisher。在最后一种情况下,您在Publisher.Output中将只有perform: closure,而不是NewValue。在这种情况下,state.text将具有旧的价值。

第二,看一下ComplexState类。我做了一个objectWillChange属性,以使文本更改手动将通知发送给订阅者。它几乎与@Published包装器一样。但是,当文本更改时,它将同时发送objectWillChange.send()textChanged.send(newValue)。这使您能够精确地选择View,以应对状态变化。如果您想要普通的行为,只需将状态放入@ObservedObject视图中的CustomStateContainer包装器中即可。然后,您将重新创建所有视图,本节也将获取更新的值:

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

如果您不希望全部重新创建,只需删除@ObservedObject。普通文本视图将停止更新,但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。

那是您需要的吗?