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