0

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

HangarRash
  • 7,314
  • 5
  • 5
  • 32
JohnSF
  • 3,736
  • 3
  • 36
  • 72
  • 1
    Did you try passing the context from the view directly to the function, `func createThingsForTestVM(count: Int, context: ModelContext)`? You could also make the function static and remove `ObservableObject` so this becomes a helper class outside of MVVM – Joakim Danielson Jun 22 '23 at 09:38
  • Very interesting. Both ways seem to work - and the static method would be a better approach. I will do some heavy testing. Thanks. And this would also be similar to malhal's suggestion below. – JohnSF Jun 22 '23 at 17:57

2 Answers2

4

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, eg you are doing something async with lifetime tied to something on screen. Now we have async await and the task modifier it is no longer needed in most cases.

malhal
  • 26,330
  • 7
  • 115
  • 133
0

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)
Mike Bedar
  • 632
  • 5
  • 14