我有一个数据模型,允许将
TodoLists
嵌套在另一个 TodoList
中。从应用程序的角度来看,我正在尝试完成类似的事情:
- Groceries
- Home
- Gardening
- Remodeling
- Kids Room
- Work
- Projects
- Training
我最初创建了一个 SwiftData 模型,可以让我做到这一点:
enum Ownership: String, CaseIterable, Codable {
case system = "SYSTEM"
case user = "USER"
}
@Model
class TodoList {
@Attribute(.unique)
var todoListID: UUID
var name: String
var ownership: Ownership
var childrenTodoList: [TodoList] = []
@Relationship(deleteRule: .cascade, inverse: \TodoList.childrenTodoList)
var parentTodoList: TodoList?
init(name: String, todoListID: UUID = UUID(), children: [TodoList] = [], parent: TodoList? = nil, ownership: Ownership) {
self.todoListID = todoListID
self.name = name
self.childrenTodoList = children
self.parentTodoList = parent
self.ownership = ownership
}
}
然后我创建了一些示例数据来镜像我试图在视图中完成的层次结构:
struct SampleData {
static let todoList: [TodoList] = {
let inbox = TodoList(name: "Inbox", ownership: .system)
let work = TodoList(name: "Work", ownership: .user)
let home = TodoList(name: "Home", ownership: .user)
let workProjects = TodoList(name: "Projects", ownership: .user)
let workTraining = TodoList(name: "Training", ownership: .user)
let homeGardening = TodoList(name: "Gardening", ownership: .user)
let homeRemodeling = TodoList(name: "Remodeling", ownership: .user)
let kidsRoom = TodoList(name: "Kids Room", ownership: .user)
work.childrenTodoList.append(contentsOf: [ workProjects, workTraining])
workProjects.parentTodoList = work
workTraining.parentTodoList = work
home.childrenTodoList.append(contentsOf: [ homeGardening, homeRemodeling ])
homeGardening.parentTodoList = home
homeRemodeling.parentTodoList = home
homeRemodeling.childrenTodoList.append(kidsRoom)
kidsRoom.parentTodoList = homeRemodeling
return [ inbox, work, home, workProjects, workTraining, homeGardening, homeRemodeling, kidsRoom ]
}()
}
在视图中,我通过
@Query
引入数据,并使用 List 和 DisclosureGroup 将其放入一系列子视图中,以实现嵌套工作。我可以创建新记录并将它们添加到 modelContext
并看到视图和子视图自动更新,这很棒。
我现在正在尝试删除数据,但遇到了一个奇怪的问题,但我没有运气解决。我正在尝试确定这是否是 SwiftData 的限制,我需要手动管理数据,或者我是否只是错误地使用了框架。
就像我提到的,在父视图中,我通过
@Query
引入数据,并使用计算属性来过滤数据以供子视图使用。这会将根 TodoList
项目发送到带有 List
的 DisclosureGroup
中,然后迭代每个根列表中的子项以创建层次结构。
@Query(sort: \TodoList.name)
var todoLists: [TodoList]
private var systemTodoLists: [TodoList] {
todoLists.filter { $0.ownership == Ownership.system && $0.parentTodoList == nil }
}
private var userTodoLists: [TodoList] {
todoLists.filter { $0.ownership == Ownership.user && $0.parentTodoList == nil }
}
private var favoriteTodoLists: [TodoList] {
todoLists.filter { $0.isFavorite }
}
其中一个视图代表列表中的一行,并具有与其关联的滑动操作。在“滑动”操作中,我有一个用于删除列表的按钮。按下后,我使用
modelContext
删除列表。
.onTapGesture {
modelContext.delete(todoList)
try! modelContext.save()
}
我有以下用例来演示我的问题以及每个用例的期望结果。
此用例顺利通过。我可以删除“Groceries”记录,并将其从视图和数据库中删除。
这个用例失败了。当我删除“工作”记录时,它会删除“工作”记录,但随后所有子记录(“项目”和“培训”)都将移动到列表的根目录,而不是级联删除。
这个用例失败了。当我删除“工作”下的“项目”记录时,它将删除“项目”及其父“工作”。 “Training”记录被提升到列表的根目录并且不会被删除。我希望删除“项目”,保留“工作”,而“培训”保持不变。
这个用例失败了。当我删除“Remodeling”记录时,它将自行删除及其父“Home”。 “Remodeling”子项“Kids Room”被单独保留并提升到列表的根。 “重塑”兄弟姐妹“园艺”被提升为列表的根。我希望“家”被单独留下,“园艺”作为“家”的孩子被单独留下。我希望删除“改造”和“儿童房”。
这个用例失败了。当我删除“家庭”记录时,它会自行删除,同时留下“改造”、“改造/儿童房”和“园艺”。我希望所有这些儿童记录都被删除。
我的数据模型做错了什么?我在
childrenTodoList
属性和 parentTodoList
属性上尝试了 @Relationship 宏的几种不同组合以获得所需的行为,但我没有成功。
这是 SwiftData 或底层 Core Data 框架的限制吗?如果是这样,我可以将数据管理(在视图中使用
@Query
)与 viewModel 和数据存储混合起来,以管理模型父/子关系中的删除和清理吗?或者我是否必须完全删除 @Query
的使用,而只通过 Container
自己加载/管理我自己的数据存储中的数据?
我在下面有一个完整的重现。您可以注释掉
onAppear
以防止在需要时重置数据库。
import SwiftUI
import SwiftData
// MARK: - Models
enum Ownership: String, CaseIterable, Codable {
case system = "SYSTEM"
case user = "USER"
}
@Model
class TodoList {
@Attribute(.unique)
var todoListID: UUID
var name: String
var ownership: Ownership
var childrenTodoList: [TodoList] = []
@Relationship(deleteRule: .cascade, inverse: \TodoList.childrenTodoList)
var parentTodoList: TodoList?
init(name: String, todoListID: UUID = UUID(), children: [TodoList] = [], parent: TodoList? = nil, ownership: Ownership) {
self.todoListID = todoListID
self.name = name
self.childrenTodoList = children
self.parentTodoList = parent
self.ownership = ownership
}
}
// MARK: - Data
struct SampleData {
static let todoList: [TodoList] = {
let inbox = TodoList(name: "Inbox", ownership: .system)
let groceries = TodoList(name: "Groceries", ownership: .user)
let work = TodoList(name: "Work", ownership: .user)
let home = TodoList(name: "Home", ownership: .user)
let workProjects = TodoList(name: "Projects", ownership: .user)
let workTraining = TodoList(name: "Training", ownership: .user)
let homeGardening = TodoList(name: "Gardening", ownership: .user)
let homeRemodeling = TodoList(name: "Remodeling", ownership: .user)
let kidsRoom = TodoList(name: "Kids Room", ownership: .user)
work.childrenTodoList.append(contentsOf: [ workProjects, workTraining])
workProjects.parentTodoList = work
workTraining.parentTodoList = work
home.childrenTodoList.append(contentsOf: [ homeGardening, homeRemodeling ])
homeGardening.parentTodoList = home
homeRemodeling.parentTodoList = home
homeRemodeling.childrenTodoList.append(kidsRoom)
kidsRoom.parentTodoList = homeRemodeling
return [ inbox, groceries, work, home, workProjects, workTraining, homeGardening, homeRemodeling, kidsRoom ]
}()
}
actor PreviewContainer {
@MainActor
static var container: ModelContainer = {
return try! inMemoryContainer()
}()
static var inMemoryContainer: () throws -> ModelContainer = {
let schema = Schema([ TodoList.self ])
let container = try! ModelContainer(
for: schema,
configurations: [ModelConfiguration(isStoredInMemoryOnly: false)])
return container
}
}
// MARK: - Views
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
var body: some View {
MainView()
.onAppear {
try! modelContext.delete(model: TodoList.self)
SampleData.todoList.forEach { modelContext.insert($0) }
try! modelContext.save()
}
}
}
struct MainView: View {
@Query(sort: \TodoList.name)
var todoLists: [TodoList]
@Environment(\.modelContext) private var modelContext
private var systemTodoLists: [TodoList] {
todoLists.filter { $0.ownership == Ownership.system && $0.parentTodoList == nil }
}
private var userTodoLists: [TodoList] {
todoLists.filter { $0.ownership == Ownership.user && $0.parentTodoList == nil }
}
var body: some View {
List {
ForEach(systemTodoLists) { list in
Text(list.name)
}
Section("Lists") {
ForEach(userTodoLists) { list in
getListContent(list)
}
}
}
}
@ViewBuilder
func getListContent(_ list: TodoList) -> some View {
if list.childrenTodoList.isEmpty {
getRowForList(list)
} else {
getRowForParentList(list)
}
}
@ViewBuilder
func getRowForParentList(_ list: TodoList) -> some View {
DisclosureGroup(isExpanded: .constant(true)) {
ForEach(list.childrenTodoList.sorted(by: { $0.name < $1.name })) { child in
getListContent(child)
}
} label: {
getRowForList(list)
}
}
func getRowForList(_ todoList: TodoList) -> some View {
HStack {
Text(todoList.name)
Spacer()
}
.contentShape(Rectangle())
.onTapGesture {
modelContext.delete(todoList)
try! modelContext.save()
}
}
}
#Preview {
ContentView()
.modelContainer(PreviewContainer.container)
}
deleteRule: .cascade
表示级联删除到指定目标。换句话说,“当我被删除时,也删除这个对象。”所以对于你写的:
var childrenTodoList: [TodoList] = []
@Relationship(deleteRule: .cascade, inverse: \TodoList.childrenTodoList)
var parentTodoList: TodoList?
这意味着当你删除一个列表时,SwiftData 也应该删除它的父级(它会级联到该列表的父级,等等,一直到树上)。这就是为什么祖先的所有兄弟姐妹都级联到根的原因 - 他们的父母被删除,因此该字段设置为
nil
。这似乎不直观,而且不太可能是您的意图。
您几乎可以肯定的意思是将删除级联到子级:
@Relationship(deleteRule: .cascade, inverse: \TodoList.parentTodoList)
var childrenTodoList: [TodoList] = []
var parentTodoList: TodoList?
这意味着当您删除列表时,SwiftData 应删除其所有子列表(及其子列表等)。它还会自动从其父节点的子节点列表中删除已删除的节点,无需您自己执行此操作。
如果您想要执行诸如将已删除列表的子级移动到其父级之类的操作,则必须手动执行此操作。