JohnSF
JohnSF

Reputation: 4300

Can't Get Reference to SwiftData ModelContainer and ModelContext in MVVM Class

I'm trying to understand the new SwiftData framework. It's pretty easy to get it to work as long as I do everything in a SwiftUI view. I'm trying to be a good coder and separate the data from the UI but have been unable to get connections to the ModelContainer and the ModelContext is a class file.

Here is an example that can be run as is:

Model:

@Model
final public class Thing: Identifiable {

    let myID = UUID()
    var name: String
    var comment: String

    init(name: String, comment: String) {
        self.name = name
        self.comment = comment
    }

}//struct

ContentView: Change the code in the Button to create 10 test records to use the VM version.

struct ContentView: View {

    @StateObject var contentVM = ContentViewModel()
    @Environment(\.modelContext) private var context

    @State private var name: String = ""
    @State private var comment: String = ""

    @State private var selection: Thing?

    @Query(sort: \.name) var things: [Thing]

    var body: some View {
        NavigationStack {
            VStack(alignment: .leading) {
                Text("Name:")
                    .padding(.leading, 12)
                TextField("name", text: $name)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding(EdgeInsets(top: 5, leading: 10, bottom: 0, trailing: 10))
            
                Text("Comment:")
                    .padding(.leading, 12)
                TextField("comment", text: $comment)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding(EdgeInsets(top: 5, leading: 10, bottom: 0, trailing: 10))
            }//v
            .padding()
            VStack(spacing: 20) {
                    Button(action: {
                        let thing = Thing(name: name, comment: comment)
                        context.insert(object: thing)
                    }, label: {
                        Text("Save")
                    })
                
                    Button(action: {
                        //contentVM.createThingsForTestVM(count: 10)
                        createThingsForTest(count: 10)
                    }, label: {
                        Text("Create 10 Test Records")
                    })
                }//v buttons

            Divider()
        
            List {
                ForEach(things) { thing in
                    Text(thing.name)
                }
                .onDelete(perform: deleteThings(at:))
            }//list
            .toolbar {
                ToolbarItemGroup(placement: .navigationBarTrailing) {
                    Button {
                        deleteAllThings()
                    } label: {
                        Image(systemName: "trash")
                    }

                }//group
            }//toolbar
        }//nav stack
    }//body

    private func deleteThings(at offsets: IndexSet) {
        withAnimation {
            offsets.map { things[$0] }.forEach(deleteThing)
        }
    }//delete at offsets

    private func deleteThing(_ thing: Thing) {

        //Unselect the item before deleting it.
        if thing.objectID == selection?.objectID {
            selection = nil
        }
        context.delete(thing)
    }//delete things

    private func deleteAllThings() {
    
        for t in things {
            if t.objectID == selection?.objectID {
                selection = nil
            }
            context.delete(t)
        }
    }//delete all things

    private func createThingsForTest(count: Int) {
    
        for i in 0..<count {
            let t = Thing(name: "Name " + String(i), comment: "Comment " + String(i))
            context.insert(object: t)
        }
    
        do {
            try context.save()
        } catch {
            print(error)
        }
    
    }//create things

}//struct content view

ContentViewModel: This does create records but it does not update the UI and it seems like the wrong approach to me. I tried to setup the ModelContainer and the ModelContext in the initializer but I was not able to make that work at all.

class ContentViewModel: ObservableObject {

    init() {}

    @MainActor
    func createThingsForTestVM(count: Int) {
    
        do {
            let container = try ModelContainer(for: Thing.self)
            let context = container.mainContext
        
            for i in 0..<count {
                let t = Thing(name: "Name " + String(i), comment: "Comment " + String(i))
                context.insert(object: t)
            }
        
            try context.save()
        
        } catch {
            print("Could not create a container \(error.localizedDescription)")
        }
    }//create things
}//class

Any guidance would be appreciated. Xcode 15.0 Beta (15A5160n), iOS 17.0

Upvotes: 5

Views: 6760

Answers (3)

malhal
malhal

Reputation: 30549

In SwiftUI the View struct is already separate from the UI and the View struct hierarchy should be your primary encapsulation mechanism for view data.

SwiftUI diffs these structs and creates/updates/removes the UI objects automatically for you, ie it manages the actual view layer or the V in MVC.

So you can just remove the custom view model object and use the View struct and property wrappers as designed.

@StateObject is for when you want a reference type in a @State, e.g. you are doing something async with lifetime tied to something on screen. It is no longer needed in most cases since we now have async/await via the .task modifier which runs while something is visible and cancels when it dissapears.

Upvotes: 5

Simon Gamble
Simon Gamble

Reputation: 61

Potentially not the way you should do it but it definitely works by registering the ModelContainer as a service using Factory.

Upvotes: 0

Mike Bedar
Mike Bedar

Reputation: 910

I'm struggling through the same stuff - one thing I noticed:

Instead of:

let container = try ModelContainer(for: Thing.self)
let context = container.mainContext

try:

let container = try ModelContainer(for: Thing.self)
let context = ModelContext(container)

Upvotes: 8

Related Questions