如何将环境中的模型注入 SwiftUI 中的 ViewModel

How to inject a Model from the Environment into a ViewModel in SwiftUI

我正在尝试对我的 SwiftUI 应用程序进行 MVVM,但无法找到一个有效的解决方案来将来自@EnvironmentObject 的共享模型注入应用程序的各种视图的 ViewModel。

下面的简化代码在示例视图的 init() 中创建了一个模型对象,但我觉得我应该在应用程序的顶部创建模型,以便它可以在多个视图之间共享并将在模型更改时触发重绘。

我的问题是这是否是正确的策略,如果是正确的,如何正确执行,如果不是,我有什么问题,我该怎么做。我还没有找到任何例子来证明这一点从头到尾都是真实的,我不知道我是否只是关闭了几个 属性 包装器,或者我正在接近这个完全错误的方法。

import SwiftUI

@main
struct DIApp: App {

// This is where it SEEMS I should be creating and sharing Model:
// @StateObject var dataModel = DataModel()

    var body: some Scene {
        WindowGroup {
            ListView()
//                .environmentObject(dataModel)
        }
    }
}

struct Item: Identifiable {
    let id: Int
    let title: String
}

class DataModel: ObservableObject {
    @Published var items = [Item]()

    init() {
        items.append(Item(id: 1, title: "First Item"))
        items.append(Item(id: 2, title: "Second Item"))
        items.append(Item(id: 3, title: "Third Item"))
    }
    
    func addItem(_ item: Item) {
        items.append(item)
        print("DM adding \(item.title)")
    }
}

struct ListView: View {
    
// Creating the StateObject here compiles, but it will not work
// in a realistic app with other views that need to share it.
// It should be an app-wide ObservableObject created elsewhere
// and accessible everywhere, right?

    @StateObject private var vm: ViewModel

    init() {
        _vm = StateObject(wrappedValue: ViewModel(dataModel: DataModel()))
    }

    var body: some View {
        NavigationView {
            List {
                ForEach(vm.items) { item in
                    Text(item.title)
                }
            }
            .navigationTitle("List")
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarItems(trailing:
                Button(action: {
                    addItem()
                }) {
                    Image(systemName: "plus.circle")
                }
            )
        }
        .navigationViewStyle(StackNavigationViewStyle())

    }
    
    func addItem() {
        vm.addRandomItem()
    }
}

extension ListView {
    class ViewModel: ObservableObject {

        @Published var items: [Item]
        let dataModel: DataModel

        init(dataModel: DataModel) {
            self.dataModel = dataModel
            items = dataModel.items
        }
        
        func addRandomItem() {
            let newID = Int.random(in: 100..<999)
            let newItem = Item(id: newID, title: "New Item \(newID)")

// The line below causes Model to be successfully updated --
// dataModel.addItem print statement happens -- but Model change
// is not reflected in View.

            dataModel.addItem(newItem)

// The line below causes the View to redraw and reflect additions, but the fact
// that I need it means I am not doing doing this right. It seems like I should
// be making changes to the Model and having them automatically update View.

            items.append(newItem)

        }
    }
}

这里有一些不同的问题和多种策略来处理它们。

从顶部开始,是的,您可以在 App 级别创建数据模型:

@main
struct DIApp: App {

    var dataModel = DataModel()

    var body: some Scene {
        WindowGroup {
            ListView(dataModel: dataModel)
                .environmentObject(dataModel)
        }
    }
}

请注意,我已将 dataModel 作为 environmentObject 显式传递给 ListView 。这是因为如果你想在 init 中使用它,它必须显式传递。但是,也许子视图也需要对它的引用,所以 environmentObject 会自动将它发送到层次结构中。

下一个问题是您的 ListView 不会更新,因为您有 嵌套 ObservableObject。如果您更改子对象(在本例中为 DataModel),除非您显式调用 objectWillChange.send().

,否则父对象不知道要更新视图
struct ListView: View {
    @StateObject private var vm: ViewModel

    init(dataModel: DataModel) {
        _vm = StateObject(wrappedValue: ViewModel(dataModel: dataModel))
    }

    var body: some View {
        NavigationView {
            List {
                ForEach(vm.dataModel.items) { item in
                    Text(item.title)
                }
            }
            .navigationTitle("List")
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarItems(trailing:
                Button(action: {
                    addItem()
                }) {
                    Image(systemName: "plus.circle")
                }
            )
        }
        .navigationViewStyle(StackNavigationViewStyle())

    }
    
    func addItem() {
        vm.addRandomItem()
    }
}

extension ListView {
    class ViewModel: ObservableObject {
        let dataModel: DataModel

        init(dataModel: DataModel) {
            self.dataModel = dataModel
        }
        
        func addRandomItem() {
            let newID = Int.random(in: 100..<999)
            let newItem = Item(id: newID, title: "New Item \(newID)")

            dataModel.addItem(newItem)
            self.objectWillChange.send()
        }
    }
}

另一种方法是将 DataModel 作为 @ObservedObject 添加到您的 ListView 中。这样,当它改变时,视图将更新,即使 ViewModel 没有任何 @Published 属性:


struct ListView: View {
    @StateObject private var vm: ViewModel
    @ObservedObject private var dataModel: DataModel

    init(dataModel: DataModel) {
        _dataModel = ObservedObject(wrappedValue: dataModel)
        _vm = StateObject(wrappedValue: ViewModel(dataModel: dataModel))
    }

    var body: some View {
        NavigationView {
            List {
                ForEach(vm.dataModel.items) { item in
                    Text(item.title)
                }
            }
            .navigationTitle("List")
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarItems(trailing:
                Button(action: {
                    addItem()
                }) {
                    Image(systemName: "plus.circle")
                }
            )
        }
        .navigationViewStyle(StackNavigationViewStyle())

    }
    
    func addItem() {
        vm.addRandomItem()
    }
}

extension ListView {
    class ViewModel: ObservableObject {
        let dataModel: DataModel

        init(dataModel: DataModel) {
            self.dataModel = dataModel
        }
        
        func addRandomItem() {
            let newID = Int.random(in: 100..<999)
            let newItem = Item(id: newID, title: "New Item \(newID)")

            dataModel.addItem(newItem)
        }
    }
}

另一个对象将使用 Combineitems 更新时自动发送 objectWilLChange 更新:

struct ListView: View {
    @StateObject private var vm: ViewModel

    init(dataModel: DataModel) {
        _vm = StateObject(wrappedValue: ViewModel(dataModel: dataModel))
    }

    var body: some View {
        NavigationView {
            List {
                ForEach(vm.dataModel.items) { item in
                    Text(item.title)
                }
            }
            .navigationTitle("List")
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarItems(trailing:
                Button(action: {
                    addItem()
                }) {
                    Image(systemName: "plus.circle")
                }
            )
        }
        .navigationViewStyle(StackNavigationViewStyle())

    }
    
    func addItem() {
        vm.addRandomItem()
    }
}

import Combine

extension ListView {
    class ViewModel: ObservableObject {
        let dataModel: DataModel
        
        private var cancellable : AnyCancellable?

        init(dataModel: DataModel) {
            self.dataModel = dataModel
            cancellable = dataModel.$items.sink { [weak self] _ in
                self?.objectWillChange.send()
            }
        }
        
        func addRandomItem() {
            let newID = Int.random(in: 100..<999)
            let newItem = Item(id: newID, title: "New Item \(newID)")

            dataModel.addItem(newItem)
        }
    }
}

如您所见,有几个选项(这些和其他选项)。您可以选择最适合您的设计模式。

您可能无法找到可行的解决方案,因为它不是有效的方法。在 SwiftUI 中,我们不使用视图模型对象的 MVVM 模式。 View 数据结构已经是 SwiftUI 用来在屏幕上创建和更新实际视图(如 UILabel 等)的视图模型。你还应该知道,当你使用 属性 包装器时,比如 @State 它使我们的超级高效视图数据结构表现得像一个对象,但没有实际堆对象的内存消耗。如果你创建了额外的对象,那么你就会减慢 SwiftUI 的速度,并且会失去依赖跟踪等魔力。

这是您的固定代码:

import SwiftUI

@main
struct DIApp: App {

 @StateObject var dataModel = DataModel()

    var body: some Scene {
        WindowGroup {
            ListView()
                .environmentObject(dataModel)
        }
    }
}

struct Item: Identifiable {
    let id: Int
    let title: String
}

class DataModel: ObservableObject {
    @Published var items = [Item]()

    init() {
        items.append(Item(id: 1, title: "First Item"))
        items.append(Item(id: 2, title: "Second Item"))
        items.append(Item(id: 3, title: "Third Item"))
    }
    
    func addItem(_ item: Item) {
        items.append(item)
        print("DM adding \(item.title)")
    }
}

struct ListView: View {

    @EnvironmentObject private var dataModel: DataModel

    var body: some View {
        NavigationView {
            List {
                // ForEach($dataModel.items) { $item in // if you want write access
                ForEach(dataModel.items) { item in
                    Text(item.title)
                }
            }
            .navigationTitle("List")
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarItems(trailing:
                Button(action: {
                    addItem()
                }) {
                    Image(systemName: "plus.circle")
                }
            )
        }
        .navigationViewStyle(StackNavigationViewStyle())

    }
    
    func addItem() {
        let newID = Int.random(in: 100..<999)
        let newItem = Item(id: newID, title: "New Item \(newID)")

        dataModel.addItem(newItem)
        
    }
}