【问题标题】:How to inject a Model from the Environment into a ViewModel in SwiftUI如何将环境中的模型注入 SwiftUI 中的 ViewModel
【发布时间】:2022-02-22 05:24:15
【问题描述】:

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

下面的简化代码在示例视图的 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)

        }
    }
}

【问题讨论】:

标签: mvvm dependency-injection swiftui


【解决方案1】:

这里有几个不同的问题以及处理它们的多种策略。

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

@main
struct DIApp: App {

    var dataModel = DataModel()

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

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

下一个问题是您的ListView 不会更新,因为您有嵌套 ObservableObjects。如果您更改子对象(本例中为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)
        }
    }
}

items 更新时,另一个对象将使用Combine 自动发送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)
        }
    }
}

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

【讨论】:

  • 这给了我一些很好的方向,谢谢。我实际上已经成功地使用了 Combine 模式,发现它更简单而且非常实用,但我也想了解 EnvironmentObject 的方式。我会看看在你的帮助下我能做些什么,但你的回答太棒了!
【解决方案2】:

您可能无法找到可行的解决方案,因为它不是有效的方法。在 SwiftUI 中,我们不使用视图模型对象的 MVVM 模式。 View 数据结构已经是 SwiftUI 用来在屏幕上创建和更新实际视图(如 UILabel 等)的视图模型。您还应该知道,当您使用像@State 这样的属性包装器时,它会使我们超级高效的 View 数据结构表现得像一个对象,但没有实际堆对象的内存占用。如果你创建了额外的对象,那么你会降低 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)
        
    }
}

【讨论】:

  • 谢谢,这是一个非常有用的提醒和工作方向。这对接受的答案同样有帮助,而且是一种补充。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-04
  • 2019-10-23
  • 1970-01-01
  • 1970-01-01
  • 2019-02-28
  • 1970-01-01
相关资源
最近更新 更多