Проблема с наблюдением за моделью SwiftData и UndoManager

Я создал минимальный пример, чтобы продемонстрировать проблему с наблюдением за моделью SwiftData и UndoManager. Этот проект включает в себя простой NavigationSplitView, сохраняемую модель Item SwiftData и включенный UndoManager.

Проблема: модель SwiftData Item можно наблюдать, как и ожидалось. Изменение даты в DetailView работает должным образом, и все связанные представления (ListElementView + DetailView) обновляются, как и ожидалось. При нажатии ⌘+Z для отмены с включенным UndoManager удаления или вставки на боковой панели становятся видимыми сразу (и правильно отслеживаются ContentView). Однако при изменении метки времени и нажатии ⌘+Z, чтобы отменить это изменение, оно не отображается должным образом и не сразу обновляется в соответствующих представлениях (ListElementView + DetailView).

Дальнейшие комментарии:

  • Операция отмены изменения значений модели (здесь: timestamp) отображается в DetailView при изменении выбора на боковой панели.
  • Отмена операции изменения значения модели (здесь: timestamp) отображается в ListElementView при перезапуске приложения.
  • Операция отмены изменений значений модели (здесь: timestamp) правильно отслеживается и сразу отображается на боковой панели при опускании ListElementView (без инкапсуляции представления).

Кажется, что UndoManager не вызывает перерисовку ContentView через запрос items?

Соответствующая база кода:

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var items: [Item]
    @State private var selectedItems: Set<Item> = []

    var body: some View {
        NavigationSplitView {
            List(selection: $selectedItems) {
                ForEach(items) { item in
                    ListElementView(item: item)
                        .tag(item)
                }
                .onDelete(perform: deleteItems)
            }
            .navigationSplitViewColumnWidth(min: 180, ideal: 200)
            .toolbar {
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        } detail: {
            if let item = selectedItems.first {
                DetailView(item: item)
            } else {
                Text("Select an item")
            }
        }
        .onDeleteCommand {
            deleteSelectedItems()
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(timestamp: Date())
            modelContext.insert(newItem)
        }
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            for index in offsets {
                modelContext.delete(items[index])
            }
        }
    }
    
    private func deleteSelectedItems() {
        for selectedItem in selectedItems {
            modelContext.delete(selectedItem)
            selectedItems.remove(selectedItem)
        }
    }
}
struct ListElementView: View {
    @Bindable var item: Item
    
    var body: some View {
        Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
    }
}
struct DetailView: View {
    @Bindable var item: Item
    
    var body: some View {
        Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
        
        DatePicker(selection: $item.timestamp, label: { Text("Change Date:") })
    }
}
@Model
final class Item {
    var timestamp: Date
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

Всегда публикуйте соответствующий код как часть вопроса, а не как внешнюю ссылку.

Joakim Danielson 03.07.2024 15:34

Возможно, это общее правило, но в данном случае нам определенно нужен был весь проект для воспроизведения и решения проблемы.

malhal 03.07.2024 15:52

@malhal Meta.stackoverflow.com/questions/254428/…

Joakim Danielson 03.07.2024 16:11

@JoakimDanielson: ОК, согласен. Я добавил наиболее подходящую базу кода.

Maschina 03.07.2024 21:29
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
70
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я вижу несколько ошибок, попробуйте это, чтобы все заработало:

Удали это:

// container.mainContext.undoManager = UndoManager()

И это:

.commands {
    // AppCommands()
}

В ContentView добавьте это:

@Environment(\.undoManager) var undoManager
...
.onChange(of: undoManager, initial: true) {
    modelContext.undoManager = undoManager
}

Для строки списка попробуйте следующее:

ListElementView(timestamp: item.timestamp)
    .tag(item)

Или удалите .tag() и вместо этого добавьте это:

NavigationLink(value: item) { // sadly causes warning "multiple updates per frame"
    ListElementView(item: item)
}

В зависимости от того, что вы выберете, измените ListElementView на:

struct ListElementView: View {
    let item: Item
    // or better: let timestamp: Date

Измените DetailView, чтобы он брал только то, что ему нужно, то есть доступ на запись к метке времени, например:

        } detail: {
            if let item = selectedItems.first {
                DetailView(timestamp: Bindable(item).timestamp)
            } else {
                Text("Select an item")
            }
        }

И это:

struct DetailView: View {
    @Binding var timestamp: Date
    
    var body: some View {
        Text(timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
        DatePicker(selection: $timestamp, label: { Text("Change Date:") })
    }
}

Благодаря этим исправлениям отмена корректно настраивается в контексте, метка боковой панели обновляется, а подробные сведения обновляются при отправке команды отмены.

Кстати, вы, возможно, обнаружили ошибку в Observable, мне кажется, detail: { DetailView(item:item) } не вызывает body, когда item.timestamp меняется, но в ListElementView(item: item) он делает. detail: всегда вёл себя довольно плохо.

Наконец, вам следует изменить инициализацию контейнера на это, чтобы это не происходило дважды, если приложение body пересчитывается, например.

static var persistent: ModelContainer = {
    ...
    }()

Да я вижу. Действительно, добавление индивидуальной привязки явно помогает, что может быть довольно неприятно при наличии нескольких параметров в более сложном DetailView. Спасибо за другие подсказки. Однако NavigationLink создает двусмысленность в отношении selectedItems. Он создает предупреждение «несколько обновлений на кадр».

Maschina 03.07.2024 14:37

Хорошо, вместо NavigationLink попробуйте ListElementView(timestamp: item.timestamp).tag(item), который обновляется правильно и не имеет предупреждения. К сожалению, List на macOS довольно глючен, и нам придется продолжать отправлять отзывы, чтобы это когда-нибудь было исправлено.

malhal 03.07.2024 15:30

Я просто подумал, что нам, вероятно, следует сравнить код, работающий в ОС iPad, чтобы определить, где находятся настоящие ошибки в macOS. В симуляторе можно выполнить команду +z с аппаратной клавиатурой. И разделенное представление появляется в альбомной ориентации.

malhal 03.07.2024 16:25

Другие вопросы по теме