Код: Выделить всё
@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 = []
}
}
Код: Выделить всё
@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())
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 в SwiftUI.
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)
}
}
}
}
Код: Выделить всё
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()
}
}
}
}
}
}
Код: Выделить всё
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")
}
}
}
}
}
Код: Выделить всё
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()
}
}
}
Код: Выделить всё
@Observable class TaskListEditorViewModel {
var name: String
var parentTaskList: TaskList?
init(taskList: TaskList) {
name = taskList.name
parentTaskList = taskList.parentList
}
}
< pre class="lang-swift Prettyprint-override">
Код: Выделить всё
#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())
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]
}()
}
Обратите внимание, что на первый взгляд может показаться, что они больше ничего не делают. Контурная группа. В моем реальном приложении все сложнее. Он упрощен для воспроизводимого примера.
Код: Выделить всё
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)
}
}
}
Код: Выделить всё
List {
ForEach(taskLists.filter({$0.parentList == nil})) { list in
TaskListRowView(taskList: list)
}
}
Если я изменю свой код сохранения так, чтобы он вручную обновлял родительский массив, проблема исчезнет, и все будет работать как положено.
Код: Выделить всё
ToolbarItem {
Button("Save") {
list.name = viewModel.name
list.parentList = viewModel.parentTaskList
// Manually add to "Home"
if let newParent = viewModel.parentTaskList {
newParent.taskLists?.append(list)
}
try! modelContext.save()
dismiss()
}
}
Почему мой явный вызов добавления решить это? Я не понимаю, как SwiftData изменил массив, и наблюдающие представления не получили уведомления об этом изменении. Мой первоначальный подход (без обновления массивов вручную) отлично работает в каждом юнит/интеграционном тесте, который я запускаю, но я не могу заставить SwiftUI наблюдать за изменениями массива.
Если кто-нибудь может объяснить, почему это это так, и независимо от того, придется ли мне вручную обновлять массивы в будущем, я был бы признателен.
У меня есть полный исходный код, доступный в виде Gist для упрощения копирования/вставки - соответствует содержанию поста.
Редактировать
Еще одна вещь, которая вызывает у меня путаницу, заключается в том, что список Finance уже существует до того, как я вручную добавил его в массив родительского списка. Итак, несмотря на то, что я все равно добавил его, SwiftData достаточно умен, чтобы не дублировать его. В этом случае я не знаю, просто заменяет ли это то, что там есть, или говорит «нет» и не добавляет это. Если он не добавляет его, то что уведомляет наблюдающие представления об изменении данных?
Подробнее здесь: https://stackoverflow.com/questions/791 ... ta-changes