Dear Apple Developer Forum
As the title suggests, I have an issue with Swift Data when I want to modify a property of a recursive model class instance. Please consider the following sample project:
import SwiftUI
import SwiftData
@main
struct ISSUEApp: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([
Item.self,
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(sharedModelContainer)
}
}
@Model
final class Item {
var name: String?
var parent: Item?
init(name: String?, parent: Item?) {
self.name = name
self.parent = parent
}
}
struct ContentView: View {
@Environment(\.modelContext) private var context
@Query private var items: [Item]
@State private var itemToMove: Item?
@State private var count: Int = 0
@State private var presentMoveView: Bool = false
var body: some View {
NavigationStack() {
List(items, id: \.id) {item in
Button(action: {
itemToMove = item
}, label: {
Text("Id: \(item.name ?? "ERROR") and my parent iD is \(item.parent?.name ?? "root")")
.bold(itemToMove == item)
.italic(itemToMove == item)
})
}
.sheet(isPresented: $presentMoveView, content: {
MoveView(toMove: self.itemToMove!)
})
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
let i = Item(name: "\(count)", parent: nil)
context.insert(i)
try? context.save()
count += 1
}, label: {
Text("Add an item")
})
}
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
presentMoveView.toggle()
}, label: {
Text("Move selected item")
})
}
}
}
}
}
struct MoveView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@Query private var items: [Item]
@Bindable var toMove: Item
@State private var selectedFutureParent: Item?
var body: some View {
NavigationStack(){
List(items, id: \.id) {item in
Button(action: {
selectedFutureParent = item
}, label: {
Text("Id: \(item.name ?? "ERROR") and my parent iD is \(item.parent?.name ?? "root")")
.bold(selectedFutureParent == item)
.italic(selectedFutureParent == item)
})
}
.toolbar(){
ToolbarItem{
Button("Move", action: {
toMove.parent = selectedFutureParent
dismiss()
})
}
}
}
}
}
#Preview {
ContentView()
.modelContainer(for: Item.self, inMemory: true)
}
Please launch the preview of this app, add items (as many as you'd like), select one and click on the "Move selected item" button. Select the new parent of the item. As you may have noticed, both selected items (moved item and new parent) are modified, whereas only one equality is used. This issue seems to be independent from the @Bindable property wrapper. I tried many things, such as using index instead of direct elements; using local let constant for the parent but the constant is still modified (very weird...)
Thank you in advance for your help ! Best regards
Based on your description, SwiftData actually behaves correctly. To elaborate, let's look at your model:
@Model
final class Item {
var name: String?
var parent: Item?
....
}
You rely on SwiftData to infer that parent
is a to-one relationship, which is fine, but as a part of the inference process, SwiftData also determines that the inverse relationship of parent
is parent
. So assuming you have two items, item1
and item2
, when you set item1.parent
to item2
, SwiftData will automatically set item2.parent
to item1
, because it sees item2.parent
as the inverse relationship of item1.parent
.
If you don't intent to have an inverse relationship, you can explicitly specify a nil
inverse relationship using Relationship:
@Model
final class Item {
var name: String?
@Relationship(deleteRule: .nullify, inverse: nil)
var parent: Item?
....
}
If your intent is to have a bi-directional to-one relationship, add an inverse relationship, as shown below:
@Model
final class Item {
var name: String?
var child: Item?
@Relationship(deleteRule: .nullify, inverse: \Item.child)
var parent: Item?
....
}
Best,
——
Ziqiao Chen
Worldwide Developer Relations.