Manngo
Manngo

Reputation: 16399

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?

Upvotes: 1

Views: 296

Answers (2)

Joakim Danielson
Joakim Danielson

Reputation: 52043

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
    }
}

Upvotes: 2

Here is my test code that works for me, using TestData for testing.

struct ContentView: View {
    let testID: String = String(UUID().uuidString.prefix(5))
    
    var body: some View {
        TestContent(testID: testID)
    }
}

struct TestContent: View {
    @Environment(\.modelContext) private var modelContext
    @Query var testData: [TestData]
    
    var testIDValue: String = ""
    var testItem: TestData
        
    init(testID: String) {
        self.testItem = TestData(id: testID, value: "New value: \(testID)")
    }
        
    var body: some View {
        VStack {
            VStack {
                Text(testIDValue)
                Text("\(testData.count)")
                EditTestItem(testData: testItem)
            }
            Button("Save", action: {
                do {
                    try modelContext.save()
                } catch {
                    print("\(#line) oops")
                }
            })
            List {
                ForEach(testData){ test in
                    HStack {
                        Text(test.id)
                        Spacer()
                        Text(test.value)
                    }
                }
            }
        }
        
        // .task {
        .onAppear {
            modelContext.insert(testItem)
            do {
                try modelContext.save()
            } catch {
                print("\(#line) oops")
            }
        }
    }
}
 
struct EditTestItem: View {
    @Bindable var testData: TestData
    
    var body: some View {
        VStack {
            HStack {
                Text(testData.id)
                TextField("Enter Data Here", text: $testData.value)
                    .border(.red)
            }
        }
    }
}


@main
struct TestApp: App {
    var sharedModelContainer: ModelContainer = {
        let schema = Schema([
            TestData.self
        ])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
    }
}

Upvotes: 1

Related Questions