20

问题

为了实现应用程序代码的简洁外观,我为每个包含逻辑的视图创建了 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 替换为@StateProperty 并int直接使用(在此示例中),它将保持持久化并且不会重新创建

struct SomeView: View {

    @State var state = 1

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

这显然不适用于更复杂的国家。如果我为@State(如 ViewModel)设置一个类,越来越多的事情不会按预期工作。

问题

  • 有没有办法不每次都重新创建 ViewModel?
  • 有没有办法复制@StatePropertywrapper @ObservedObject
  • 为什么@State 将状态保留在重绘上?

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

重复的问题

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

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

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

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

4

4 回答 4

15

最后,苹果提供了一个解决方案:@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正在重置。

于 2020-06-22T22:30:07.770 回答
6

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

于 2020-05-25T17:44:41.990 回答
2

有没有办法不每次都重新创建 ViewModel?

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

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

有没有办法为@ObservedObject 复制@State Propertywrapper?

没有需求。@ObservedObjectis-a 已经DynamicProperty类似于@State

为什么@State 将状态保留在重绘上?

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

于 2020-05-08T11:52:50.920 回答
0

您需要PassThroughSubjectObservableObject课堂上提供自定义。看看这段代码:

//
//  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()
    }
}

首先,我使用在View中传递to的TextChanger新值。请注意,在这种情况下得到,而不是. 在最后一种情况下,您将只有in ,而不是 NewValue。在这种情况下将具有旧值。.text.onReceive(...)CustomStateonReceivePassthroughSubjectObservableObjectPublisherPublisher.Outputperform: closurestate.text

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

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。

那是你需要的吗?

于 2020-05-08T13:17:51.890 回答