如何将环境中的模型注入 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)
}
}
}
另一个对象将使用 Combine
在 items
更新时自动发送 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)
}
}
我正在尝试对我的 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)
}
}
}
另一个对象将使用 Combine
在 items
更新时自动发送 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)
}
}