Я создал минимальный пример, чтобы продемонстрировать проблему с наблюдением за моделью 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
}
}
Возможно, это общее правило, но в данном случае нам определенно нужен был весь проект для воспроизведения и решения проблемы.
@malhal Meta.stackoverflow.com/questions/254428/…
@JoakimDanielson: ОК, согласен. Я добавил наиболее подходящую базу кода.
Я вижу несколько ошибок, попробуйте это, чтобы все заработало:
Удали это:
// 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. Он создает предупреждение «несколько обновлений на кадр».
Хорошо, вместо NavigationLink
попробуйте ListElementView(timestamp: item.timestamp).tag(item)
, который обновляется правильно и не имеет предупреждения. К сожалению, List
на macOS довольно глючен, и нам придется продолжать отправлять отзывы, чтобы это когда-нибудь было исправлено.
Я просто подумал, что нам, вероятно, следует сравнить код, работающий в ОС iPad, чтобы определить, где находятся настоящие ошибки в macOS. В симуляторе можно выполнить команду +z с аппаратной клавиатурой. И разделенное представление появляется в альбомной ориентации.
Всегда публикуйте соответствующий код как часть вопроса, а не как внешнюю ссылку.