一对多循环引用删除有意想不到的副作用

问题描述 投票:0回答:1

我有一个数据模型,允许将

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()
    }

我有以下用例来演示我的问题以及每个用例的期望结果。

应用任何用例之前的数据层次结构的屏幕截图。 enter image description here

用例#1:没有父母,没有孩子

此用例顺利通过。我可以删除“Groceries”记录,并将其从视图和数据库中删除。 enter image description here

用例#2:没有父母,有孩子

这个用例失败了。当我删除“工作”记录时,它会删除“工作”记录,但随后所有子记录(“项目”和“培训”)都将移动到列表的根目录,而不是级联删除。 enter image description here

用例#3:有父母,没有孩子

这个用例失败了。当我删除“工作”下的“项目”记录时,它将删除“项目”及其父“工作”。 “Training”记录被提升到列表的根目录并且不会被删除。我希望删除“项目”,保留“工作”,而“培训”保持不变。 enter image description here

用例#4:有父母,有孩子

这个用例失败了。当我删除“Remodeling”记录时,它将自行删除及其父“Home”。 “Remodeling”子项“Kids Room”被单独保留并提升到列表的根。 “重塑”兄弟姐妹“园艺”被提升为列表的根。我希望“家”被单独留下,“园艺”作为“家”的孩子被单独留下。我希望删除“改造”和“儿童房”。 enter image description here

用例#5:没有父母,有孩子和孙子

这个用例失败了。当我删除“家庭”记录时,它会自行删除,同时留下“改造”、“改造/儿童房”和“园艺”。我希望所有这些儿童记录都被删除。 enter image description here

我的数据模型做错了什么?我在

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

swift swiftui swiftdata
1个回答
0
投票

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 应删除其所有子列表(及其子列表等)。它还会自动从其父节点的子节点列表中删除已删除的节点,无需您自己执行此操作。

如果您想要执行诸如将已删除列表的子级移动到其父级之类的操作,则必须手动执行此操作。

© www.soinside.com 2019 - 2024. All rights reserved.