如何使用 MVVM 使用 @ObservedObject 触发自动 SwiftUI 更新

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

我有一个关于 SwiftUI 和 MVVM 组合的问题。

在开始之前,我读过一些讨论 SwiftUI 和 MVVM 结合是否有必要的文章。但我不想在这里讨论这个,因为它已经在其他地方讨论过了。我只是想知道是否可能,如果可能,如何实现。 :)

代码来了。我尝试在更新的对象类之间添加 ViewModel 层,该对象类包含按下按钮时应更新的数字。问题是,一旦我将 ViewModel 层放在中间,按下按钮时 UI 就不会自动更新。

查看


struct ContentView: View {
    
    @ObservedObject var viewModel = ViewModel()
    @ObservedObject var numberStorage = NumberStorage()
    
    var body: some View {
        VStack {
//            Text("\(viewModel.getNumberObject().number)")
//                .padding()
//            Button("IncreaseNumber") {
//                viewModel.increaseNumber()
//            }
            Text("\(numberStorage.getNumberObject().number)")
                .padding()
            Button("IncreaseNumber") {
                numberStorage.increaseNumber()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

视图模型


class ViewModel: ObservableObject {
    
    @Published var number: NumberStorage
    
    init() {
        self.number = NumberStorage()
    }
    
    func increaseNumber() {
        self.number.increaseNumber()
    }
    
    func getNumberObject() -> NumberObject {
        self.number.getNumberObject()
    }
    
} 

型号


class NumberStorage:ObservableObject {
    @Published var numberObject: NumberObject
    
    init() {
        numberObject = NumberObject()
    }
    
    public func getNumberObject() -> NumberObject {
        return self.numberObject
    }
    
    public func increaseNumber() {
        self.numberObject.number+=1
    }
}

struct NumberObject: Identifiable {
    let id = UUID()
    var number = 0
} ```

Looking forward to your feedback!
swift mvvm swiftui viewmodel observedobject
4个回答
4
投票

我认为你的代码破坏了 MVVM,因为你正在向视图公开存储模型。在 MVVM 中,您的 ViewModel 应该只包含两件事:

  1. 您的视图应显示的值。这些值应该使用绑定系统自动更新(在您的情况下,是组合)
  2. 视图可能产生的事件(在您的情况下,点击按钮) 考虑到这一点,您的 ViewModel 应该wrapadaptencapsulate您的模型。我们不希望模型更改影响视图。这是一种干净的方法,可以做到: 查看:

struct ContentView: View {
    
    @StateObject // When the view creates the object, it must be a state object, or else it'll be recreated every time the view is recreated
    private var viewModel = ViewModel()
    
    var body: some View {
        VStack {
            Text("\(viewModel.currentNumber)") // We don't want to use functions here, as that will create a new object , as SwiftUI needs the same reference in order to keep track of changes
                .padding()
            Button("IncreaseNumber") {
                viewModel.increaseNumber()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

视图模型:


class ViewModel: ObservableObject {
    
    @Published
    private(set) var currentNumber: Int = 0 // Private set indicates this should only be mutated by the viewmodel
    private let numberStorage = NumberStorage()
    
    init() {
        numberStorage.currentNumber
            .map { $0.number }
        .assign(to: &$currentNumber) // Here we're binding the current number on the storage to the published var that the view is listening to.`&$` basically assigns it to the publishers address
    }
    
    func increaseNumber() {
        self.numberStorage.increaseNumber()
    }
}

型号:

class NumberStorage {
    private let currentNumberSubject = CurrentValueSubject<NumberObject, Never>(NumberObject())

    var currentNumber: AnyPublisher<NumberObject, Never> {
        currentNumberSubject.eraseToAnyPublisher()
    }
    
   func increaseNumber() {
       let currentNumber = currentNumberSubject.value.number
       currentNumberSubject.send(.init(number: currentNumber + 1))
    }
}


struct NumberObject: Identifiable { // I'd not use this, just send and int directly
    let id = UUID()
    var number = 0
}

1
投票

这是一个已知问题。 SwiftUI 尚不支持嵌套可观察对象。我认为这里不需要 ViewModel+Model,因为 ViewModel 似乎就足够了。

要实现此功能,您必须在触发模型的

objectWillChange
时手动触发 viewModel 的
objectWillChange

class ViewModel: ObservableObject {
    init() {
        number.objectWillChange.sink { [weak self] (_) in
            self?.objectWillChange.send()
        }.store(in: &cancellables)
    }
}

如果不需要的话,你最好只听你关心的对象,而不是整个可观察类。

加:

由于您不是注入,而是在视图中初始化 viewModel,所以最好使用

StateObject
而不是
ObservedObject
。请参阅 Apple 文档中的参考:在应用程序中管理模型数据


0
投票

处理此问题的一种方法是观察

Storage
类中的发布者,并在
objectWillChange
发布者发生更改时发送该发布者。我在个人项目中通过添加一个我的所有视图模型继承的类来完成此操作,该类提供了一个很好的界面并处理如下所示的组合内容:

父视图模型

import Combine

class ViewModel: ObservableObject {
    private var cancellables: Set<AnyCancellable> = []

    func publish<T>(on publisher: Published<T>.Publisher) {
        publisher.sink { [weak self] _ in self?.objectWillChange.send() }
            .store(in: &cancellables)
    }
}

具体ViewModel

class ContentViewModel: ViewModel {
    private let numberStorage = NumberStorage()

    var number: Int { numberStorage.numberObject.number }

    override init() {
        super.init()
        publish(on: numberStorage.$numberObject)
    }

    func increaseNumber() {
        numberStorage.increaseNumber()
    }
}

查看

struct ContentView: View {
    @StateObject var viewModel = ContentViewModel()

    var body: some View {
        VStack {
            Text("\(viewModel.number)")
                .padding()
            Button("IncreaseNumber") {
                viewModel.increaseNumber()
            }
        }
    }
}

模型/存储

class NumberStorage:ObservableObject {
    @Published var numberObject: NumberObject

    init() {
        numberObject = NumberObject()
    }

    public func increaseNumber() {
        self.numberObject.number += 1
    }
}

struct NumberObject: Identifiable {
    let id = UUID()
    var number = 0
}

这会导致视图在任何时候

Storage.numberObject
发生变化时重新渲染。


0
投票

我有两种方法来处理这种情况

  1. 使用 ObservableObject:

    ViewModel 类:ObservableObject { @Published var isSelected: Bool = false } 结构ContentView:视图{ @StateObject 私有 var viewModel = ViewModel() var body: 一些视图 { 切换(“选择”,isOn:$viewModel.isSelected) .onReceive(viewModel.$isSelected) { newValue 中 print("isSelected 更改为 (newValue)") } } }

  2. 没有 ObservableObject:

    类视图模型{ @Published var isSelected: Bool = false } 结构ContentView:视图{ 私有变量 viewModel = ViewModel() @State private var isSelected: Bool = false 私有变量可取消:AnyCancellable? var body: 一些视图 { 切换(“选择”,isOn:$ isSelected) .onChange(of: isSelected) { newValue 中 viewModel.isSelected = newValue } .onReceive(viewModel.$isSelected) { newValue 中 已选择 = 新值 print("isSelected 更改为 (newValue)") } } }

© www.soinside.com 2019 - 2024. All rights reserved.