swiftuiswiftdata

Inserting into SwiftData


I am trying to write some code which inserts a new item into SwiftData if it doesn’t already exist. I’m getting errors regarding the ModelContext which I can’t work out.

The following code is a stripped-down version which is enough to generate the errors. I’ve removed the logic to test for an existing item until I can get past this problem.

Sorry it’s so long:

import SwiftUI
import SwiftData

@Model
class TestData {
    @Attribute(.unique) var id: String
    var value: String
    init(id: String, value: String) {
        self.id = id
        self.value = value
    }
}

struct EditTestItem: View {
    @Bindable var testData: TestData
    var body: some View {
        VStack {
            HStack {
                Text($testData.id)
                TextField("Enter Data Here", text: $testData.value)
            }
        }
        .modelContainer(for: TestData.self)
    }
}

struct TestContent: View {
    var testIDValue: String = ""
    @Environment(\.modelContext) private var modelContext
    @Query var testData: [TestData]
    var testItem: TestData
        
    init(testID: String) {
        testItem = TestData(id: testID, value: "New value: \(testID)")

        let _ = modelContext.insert(testItem)
                    
        do {
            try modelContext.save()
        } catch {
            print("\(#line) oops")
        }
    }
        
    var body: some View {
        VStack {
            VStack {
                Text(testIDValue)
                Text("\(testData.count)")
                EditTestItem(testData: testItem)
            }
            Button("Save", action: {
                try? modelContext.save()
            })
        }
        .modelContainer(for: TestData.self)
        .onAppear {

        }
    }
}

#Preview {
    TestContent(testID: "b")
        .modelContainer(for: TestData.self)
        .frame(width: 400)
}

The errors include:

Accessing Environment's value outside of being installed on a View. This will always read the default value and will not update.

and

Set a .modelContext in view's environment to use Query

It’s certainly not adding data.

What is the correct way of doing this?


Solution

  • I would create the test object in onAppear rather than using init.

    For clarity lets have all related code in a single function that checks for an existing object and creates a new one if no one exists

    private func load(with id: String) -> TestData? {
        do {
            var object = try modelContext.fetch(FetchDescriptor<TestData>(predicate: #Predicate { $0.id == id })).first
    
            if object == nil {
                object = TestData(id: id, value: "New value: \(id)")
                modelContext.insert(object!)
                try modelContext.save()
            }
    
            return object
        } catch {
            print(error)
            return nil
        }
    }
    

    Then we need to do some changes to the view to hold the id and the object and call the new function

    struct TestContent: View {
        let testID: String
        @Environment(\.modelContext) private var modelContext
        @Query var testData: [TestData]
        @State var testItem: TestData?
    
        init(testID: String) {
            self.testID = testID
        }
    
        var body: some View {
            VStack {
                VStack {
                    // other components...
                    if let testItem {
                        EditTestItem(testData: testItem)
                    }
                }
                Button("Save", action: {
                    try? modelContext.save()
                })
            }
            .onAppear {
                testItem = load(with: testID)
            }
        }
    
        private func load(with id: String) -> TestData? {
            // as above
        }
    }