SwiftUI无法自动观察SwiftData关系更新的原因及解决方案咨询
SwiftUI无法自动观察SwiftData关系更新的原因及解决方案咨询
我最近在做一个基于SwiftData和SwiftUI的任务列表应用,遇到了一个让我摸不着头脑的问题,想请教大家。
先给大家看一下我的SwiftData模型:
@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 = [] } }
我写了测试用例验证双向关系的自动维护:当我给子列表设置parentList后,父列表的taskLists数组会自动添加这个子列表,删除子列表时父数组也会自动更新,测试结果完全符合预期:
@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) }
初始工作版本(用OutlineGroup)
一开始我用原生的OutlineGroup来展示嵌套列表,一切正常——比如把"Finance"从"Work"移动到"Home"时,UI会实时更新。相关代码如下:
ContentView
struct ContentView: View { @Query(sort: \TaskList.name) var taskLists: [TaskList] @State private var selectedList: TaskList? var body: some View { NavigationStack { List { ForEach(taskLists.filter({$0.parentList == nil})) { list in OutlineGroup(list, children: \.taskLists) { list in Text(list.name) .onTapGesture { selectedList = list } } } } .sheet(item: $selectedList, onDismiss: { selectedList = nil }) { list in TaskListEditorScreen(existingList: list) } } } }
TaskListEditorScreen
struct TaskListEditorScreen: View { @Environment(\.dismiss) private var dismiss @Environment(\.modelContext) private var modelContext @State private var viewModel: TaskListEditorViewModel @Bindable var list: TaskList init(existingList: TaskList) { list = existingList viewModel = TaskListEditorViewModel(taskList: existingList) } var body: some View { NavigationView { TaskListFormView(viewModel: viewModel) .toolbar { ToolbarItem { Button("Cancel") { dismiss() } } ToolbarItem { Button("Save") { list.name = viewModel.name list.parentList = viewModel.parentTaskList try! modelContext.save() dismiss() } } } } } }
其他配套视图
TaskListFormView
struct TaskListFormView: View { @Bindable var viewModel: TaskListEditorViewModel var body: some View { VStack { Form { TextField("Name", text: $viewModel.name) NavigationLink { TaskListPickerScreen(viewModel: self.viewModel) } label: { Text(self.viewModel.parentTaskList?.name ?? "Parent List") } } } } }
TaskListPickerScreen
struct TaskListPickerScreen: View { @Environment(\.dismiss) private var dismiss @Query(filter: #Predicate { $0.parentList == nil }, sort: \TaskList.name) private var taskLists: [TaskList] @Bindable var viewModel: TaskListEditorViewModel var body: some View { List { ForEach(taskLists) { list in OutlineGroup(list, children: \.taskLists) { child in getRowForChild(child) } } } .toolbar { ToolbarItem { Button("Clear Parent") { viewModel.parentTaskList = nil dismiss() } } } } @ViewBuilder func getRowForChild(_ list: TaskList) -> some View { HStack { Text(list.name) } .onTapGesture { if list.name == viewModel.name { return } self.viewModel.parentTaskList = list dismiss() } } }
TaskListEditorViewModel
@Observable class TaskListEditorViewModel { var name: String var parentTaskList: TaskList? init(taskList: TaskList) { name = taskList.name parentTaskList = taskList.parentList } }
数据容器与测试数据
#Preview { ContentView() .modelContext(DataContainer.preview.dataContainer.mainContext) } @MainActor class DataContainer { let schemaModels = Schema([ TaskList.self ]) let dataConfiguration: ModelConfiguration static let shared = DataContainer() static let preview = DataContainer(memoryDB: true) init(memoryDB: Bool = false) { dataConfiguration = ModelConfiguration(isStoredInMemoryOnly: memoryDB) } lazy var dataContainer: ModelContainer = { do { let container = try ModelContainer(for: schemaModels) seedData(context: container.mainContext) return container } catch { fatalError("\(error.localizedDescription)") } }() func seedData(context: ModelContext) { let lists = try! context.fetch(FetchDescriptor<TaskList>()) if lists.count == 0 { Task { @MainActor in SampleData.taskLists.filter({ $0.parentList == nil }).forEach { context.insert($0) } try! context.save() } } } } struct SampleData { static let taskLists: [TaskList] = { let home = TaskList(name: "Home") let work = TaskList(name: "Work") let remodeling = TaskList(name: "Remodeling", parentTaskList: home) let kidsBedroom = TaskList(name: "Kids Room", parentTaskList: remodeling) let livingRoom = TaskList(name: "Living Room", parentTaskList: remodeling) let management = TaskList(name: "Management", parentTaskList: work) let finance = TaskList(name: "Finance", parentTaskList: work) return [home, work, remodeling, kidsBedroom, livingRoom, management, finance] }() }
问题出现:自定义嵌套视图替代OutlineGroup后
因为需要自定义行布局和交互,我用自己写的嵌套视图替换了OutlineGroup,相关视图代码如下:
struct TaskListRowContentView: View { @Environment(\.modelContext) private var modelContext @Bindable var taskList: TaskList @State var isShowingEditor: Bool = false init(taskList: TaskList) { self.taskList = taskList } var body: some View { HStack { Text(taskList.name) } .contextMenu { Button("Edit") { isShowingEditor = true } } .sheet(isPresented: $isShowingEditor) { TaskListEditorScreen(existingList: taskList) } } } struct TaskListRowParentView: View { @Bindable var taskList: TaskList @State private var isExpanded: Bool = true var children: [TaskList] { taskList.taskLists!.sorted(by: { $0.name < $1.name }) } var body: some View { DisclosureGroup(isExpanded: $isExpanded) { ForEach(children) { child in if child.taskLists!.isEmpty { TaskListRowContentView(taskList: child) } else { TaskListRowParentView(taskList: child) } } } label: { TaskListRowContentView(taskList: self.taskList) } } } struct TaskListRowView: View { @Bindable var taskList: TaskList var body: some View { if taskList.taskLists!.isEmpty { TaskListRowContentView( taskList: taskList) } else { TaskListRowParentView(taskList: taskList) } } }
然后更新了ContentView的列表部分:
List { ForEach(taskLists.filter({$0.parentList == nil})) { list in TaskListRowView(taskList: list) } }
这时候问题来了:当我把"Finance"从"Work"移动到"Home"时,调试能看到SwiftData已经自动更新了两个父列表的taskLists数组——Work的数组长度减1,Home的数组长度加1,重启App后UI也能显示正确的结构,但实时UI完全没反应。
直到我在保存代码里手动加了一行newParent.taskLists?.append(list),UI才会实时更新,哪怕调试时看到这个数组里已经存在"Finance"了:
ToolbarItem { Button("Save") { list.name = viewModel.name list.parentList = viewModel.parentTaskList // 手动添加到新父列表,不加这行UI就不更新 if let newParent = viewModel.parentTaskList { newParent.taskLists?.append(list) } try! modelContext.save() dismiss() } }
我的困惑
- 为什么SwiftData自动维护的数组变化(父列表的
taskLists更新)没有被SwiftUI观察到?测试里明明是正常的,而且数据库也确实更新了。 - 为什么手动执行
append(哪怕元素已经在数组里,SwiftData不会重复添加)就能触发UI更新?这一步到底做了什么让SwiftUI感知到变化? - 我以后是不是必须手动维护这些双向关系的数组,才能让SwiftUI实时更新?
备注:内容来源于stack exchange,提问作者Johnathon Sullinger
相关产品推荐
相关产品推荐

