I have an app with the following model:
@Model class TaskList {
@Attribute(.unique)
var name: String
// Relationships
var parentList: TaskList?
@Relationship(deleteRule: .cascade, inverse: \TaskList.parentList)
var taskLists: [TaskList]?
init(name: String, parentTaskList: TaskList? = nil) {
self.name = name
self.parentList = parentTaskList
self.taskLists = []
}
}
If I run the following test, I get the expected results - Parent
has it's taskLists
array updated to include the Child
list created. I don't explicitly add the child to the parent array - the parentList
relationship property on the child causes SwiftData to automatically perform the append into the parent array:
@Test("TaskList with children with independent saves are in the database")
func test_savingRootTaskIndependentOfChildren_SavesAllTaskLists() async throws {
let modelContext = TestHelperUtility.createModelContext(useInMemory: false)
let parentList = TaskList(name: "Parent")
modelContext.insert(parentList)
try modelContext.save()
let childList = TaskList(name: "Child")
childList.parentList = parentList
modelContext.insert(childList)
try modelContext.save()
let fetchedResults = try modelContext.fetch(FetchDescriptor<TaskList>())
let fetchedParent = fetchedResults.first(where: { $0.name == "Parent"})
let fetchedChild = fetchedResults.first(where: { $0.name == "Child" })
#expect(fetchedResults.count == 2)
#expect(fetchedParent?.taskLists.count == 1)
#expect(fetchedChild?.parentList?.name == "Parent")
#expect(fetchedChild?.parentList?.taskLists.count == 1)
}
I have a subsequent test that deletes the child and shows the parent array being updated accordingly.
With this context in mind, I'm not seeing these relationship updates being observed within SwiftUI. This is an app that reproduces the issue. In this example, I am trying to move "Finance" from under the "Work" parent and into the "Home" list.
I have a List that loops through a @Query var taskList: [TaskList]
array. It creates a series of children views and passes the current TaskList
element down into the view as a binding.
When I perform the operation below the "Finance" element is removed from the "Work" item's taskLists
array automatically and the view updates to show the removal within the List
. In addition to that, the "Home" item also shows "Finance" within it's taskLists
array - showing me that SwiftData is acting how it is supposed to - removed the record from one array and added it to the other.
The View does not reflect this however. While the view does update and show "Finance" being removed from the "Work" list, it does not show the item being added to the "Home" list. If I kill the app and relaunch I can then see the "Finance" list within the "Home" list. From looking at the data in the debugger and in the database, I've confirmed that SwiftData is working as intended. SwiftUI however does not seem to observe the change.
ToolbarItem {
Button("Save") {
list.name = viewModel.name
list.parentList = viewModel.parentTaskList
try! modelContext.save()
dismiss()
}
}
To troubleshoot this, I modified the above code so that I explicitly add the "Finance" list to the "Home" items taskLists
array.
ToolbarItem {
Button("Save") {
list.name = viewModel.name
list.parentList = viewModel.parentTaskList
if let newParent = viewModel.parentTaskList {
// MARK: Bug - This resolves relationship not being reflected in the View
newParent.taskLists?.append(list)
}
try! modelContext.save()
dismiss()
}
}
Why does my explicit append
call solve for this? My original approach (not manually updating the arrays) works fine in every unit/integration test I run but I can't get SwiftUI to observe the array changes.
Even more strange is that when I look at viewModel.parentTaskList.taskLists
in this context, I can see that the list
item already exists in it. So my code effectively tries to add it a second time, which SwiftData is smart enough to prevent from happening. When I do this though, SwiftUI observes a change in the array and the UI reflects the desired state.
In addition to this, if I replace my custom list rows with an OutlineGroup
this issue doesn't manifest itself. SwiftUI stays updated to match SwiftData when I remove my explicit array addition.
I don't understand why my views, which is passing the TaskList
all the way down the stack via Bindable
is not updating while an OutlineGroup
does.
I have a complete reproducible ContentView
file that demonstrates this as a Gist. I tried to provide the source here but it was to much for the post.
One other anecdote. When I navigate to the TaskListEditorScreen
and open the TaskListPickerScreen
I get the following series of errors:
error: the replacement path doesn't exist: "/var/folders/07/3px_03md30v9n105yh3rqzvw0000gn/T/swift-generated-sources/@_swiftmacro_09SwiftDataA22UIChangeDetectionIssue20TaskListPickerScreenV9taskLists33_A40669FFFCF66BB4EEA5302BB5ED59CELL5QueryfMa.swift"
I saw another post regarding these and I'm wondering if my issue is related to this.
So my question is, do I need to handle observation of SwiftData models containing arrays differently in my custom views? Why do bindings not observe changes made by SwiftData but they observe changes made explicitly by me?