SwiftUI не наблюдает за изменениями SwiftDataIOS

Программируем под IOS
Ответить Пред. темаСлед. тема
Anonymous
 SwiftUI не наблюдает за изменениями SwiftData

Сообщение Anonymous »

У меня есть приложение следующей модели:

Код: Выделить всё

@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 = []
}
}
Если я запущу следующий тест, я получу ожидаемые результаты: родительский обновил массив TaskLists, включив в него созданный дочерний список. Я не добавляю дочерний элемент в родительский массив явным образом — свойство связиparentList для дочернего элемента заставляет SwiftData автоматически выполнять добавление в родительский массив:

Код: Выделить всё

    @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)
}
У меня есть последующий тест, который удаляет дочерний элемент и показывает соответствующее обновление родительского массива. Учитывая этот контекст, я не вижу, чтобы эти обновления отношений наблюдались в SwiftUI. Это приложение, которое воспроизводит проблему. В этом примере я пытаюсь переместить «Финансы» из родительского списка «Работа» в список «Главный».
Изображение

Для начала следующий код представляет собой рабочий пример, где поведение делает то, что я ожидаю — оно перемещает список от одного родителя к другому без каких-либо проблем. Это делается с помощью встроенной 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)
}
}
}
}
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
}
}
Вы можете настроить следующий контейнер и заполнить его тестовыми данными, чтобы убедиться, что в результате списки могут перемещаться между родительскими элементами, и SwiftUI обновляет его соответствующим образом.
< 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]
}()
}
Однако мне нужно настроить макет и взаимодействие каждой строки, включая способ обработки индикаторов. Чтобы облегчить это, я заменил использование 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, чтобы использовать их вместо OutlineGroup.

Код: Выделить всё

List {
ForEach(taskLists.filter({$0.parentList == nil})) { list in
TaskListRowView(taskList: list)
}
}
После внесения этого изменения у меня возникла проблема в SwiftUI. Я перенесу список «Финансы» из списка «Работа» через редактор в родительский элемент «Домой». Во время отладки я могу убедиться, что вызов modelContext.save(), который я выполняю, немедленно приводит к обновлению обоих родительских списков. Work.taskLists уменьшается на 1, а массив home.taskLists увеличивается на 1, как и ожидалось. Я могу закрыть приложение и перезапустить его, и я вижу финансовый список как дочерний элемент домашнего списка. Однако я не вижу, чтобы это отражалось в реальном времени. Мне придется закрыть приложение, чтобы увидеть изменения.
Если я изменю свой код сохранения так, чтобы он вручную обновлял родительский массив, проблема исчезнет, ​​и все будет работать как положено.

Код: Выделить всё

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()
}
}
Это меня смутило. Когда я отлаживаю это, я вижу, что viewModel.parentTaskList.taskLists.count равен 2 (Ремоделирование, Финансы). Я могу распечатать содержимое массива, и обе модели Remodeling и Finance находятся там, как и ожидалось. Однако пользовательский интерфейс не будет работать, если я явно не вызову newParent.taskLists?.append(list). Список уже существует в массиве, но я должен сделать это, чтобы SwiftUI обновил его привязку.
Почему мой явный вызов добавления решить это? Я не понимаю, как SwiftData изменил массив, и наблюдающие представления не получили уведомления об этом изменении. Мой первоначальный подход (без обновления массивов вручную) отлично работает в каждом юнит/интеграционном тесте, который я запускаю, но я не могу заставить SwiftUI наблюдать за изменениями массива.
Если кто-нибудь может объяснить, почему это это так, и независимо от того, придется ли мне вручную обновлять массивы в будущем, я был бы признателен.
У меня есть полный исходный код, доступный в виде Gist для упрощения копирования/вставки - соответствует содержанию поста.
Редактировать
Еще одна вещь, которая вызывает у меня путаницу, заключается в том, что список Finance уже существует до того, как я вручную добавил его в массив родительского списка. Итак, несмотря на то, что я все равно добавил его, SwiftData достаточно умен, чтобы не дублировать его. В этом случае я не знаю, просто заменяет ли это то, что там есть, или говорит «нет» и не добавляет это. Если он не добавляет его, то что уведомляет наблюдающие представления об изменении данных?

Подробнее здесь: https://stackoverflow.com/questions/791 ... ta-changes
Реклама
Ответить Пред. темаСлед. тема

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

  • Похожие темы
    Ответы
    Просмотры
    Последнее сообщение
  • SwiftUI не наблюдает за изменениями SwiftData
    Anonymous » » в форуме IOS
    0 Ответы
    21 Просмотры
    Последнее сообщение Anonymous
  • SwiftUI наблюдает за изменениями из подпредставления
    Anonymous » » в форуме IOS
    0 Ответы
    25 Просмотры
    Последнее сообщение Anonymous
  • SwiftUI наблюдает за изменениями в вычисляемом свойстве
    Anonymous » » в форуме IOS
    0 Ответы
    19 Просмотры
    Последнее сообщение Anonymous
  • SwiftData Custom Migration всегда терпит неудачу - возможна ошибка в SwiftData
    Гость » » в форуме IOS
    0 Ответы
    33 Просмотры
    Последнее сообщение Гость
  • SwiftData Custom Migration всегда терпит неудачу - возможна ошибка в SwiftData
    Anonymous » » в форуме IOS
    0 Ответы
    9 Просмотры
    Последнее сообщение Anonymous

Вернуться в «IOS»