I am trying to MVVM my SwiftUI app, but am unable to find a working solution for injecting a shared Model from @EnvironmentObject into the app's various Views' ViewModels.
The simplified code below creates a Model object in the init() of an example View, but I feel like I am supposed to be creating the model at the top of the app so that it can be shared among multiple Views and will trigger redraws when Model changes.
My question is whether this is the correct strategy, if so how to do it right, and if not what do I have wrong and how do I do it instead. I haven't found any examples that demonstrate this realistically beginning to end, and I can't tell if I am just a couple of property wrappers off, or it I am approaching this completely wrong.
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)
}
}
}