I am encountering an issue with the UndoManager functionality in a SwiftUI application that integrates SwiftData for persistence. This issue occurs specifically in macOS 14 (Sonoma) but works as expected on macOS 15 (Sequoia).
The focused test app I have prepared for demonstration allows users to create ParentItem objects, and for each ParentItem, users can add multiple ChildItem objects. The undo functionality (via Cmd+Z) is not working as expected in Sonoma. When I try to undo a ChildItem addition, the UndoManager does not revert just the last ChildItem added, but instead removes all ChildItems that were added in that session.
Expected Behavior
On macOS 14 (Sonoma), I expect the UndoManager to undo only the most recent transaction (in this case, a single ChildItem insert), similar to how it functions on macOS 15 (Sequoia). Each ChildItem insertion should be treated as a separate undoable action.
Current Behavior
In macOS Sonoma, pressing Cmd+Z undoes the entire list of ChildItems added to a ParentItem in the current session, rather than just the most recent ChildItem. This appears to be an issue with undo grouping, but I’ve confirmed that no explicit grouping is being used.
Question
Is this an issue with UndoManager in macOS Sonoma, particularly in how it interacts with SwiftData persistence? What changes should I make to ensure that each ChildItem insert is treated as an individual undo action in macOS Sonoma, just as it works in Sequoia?
Any guidance on isolating the issue or recommended workarounds would be appreciated. I would expect that undo actions for each child addition would be treated as separate transactions, not grouped.
Steps Taken to Solve the Problem
I attempted to manually save the model context (modelContext.save()) after each ChildItem insert to ensure proper persistence.
I also verified that UndoManager was not grouping operations explicitly by calling beginUndoGrouping() or endUndoGrouping() myself.
This issue seems to be tied specifically to macOS Sonoma, as it does not occur on macOS Sequoia, where undoing behaves as expected.
Conditions
macOS 14 Sonoma: The issue occurs consistently.
macOS 15 Sequoia: The issue does not occur.
This issue appears to be independent of hardware, as I’ve tested it on multiple machines.
APIs/Features Potentially Involved
UndoManager in a SwiftUI application
SwiftData for persistence (using modelContext.save())
macOS version-specific behavior
Steps to reproduce
Clone test project (https://github.com/Maschina/SwiftDataUndoManagerExample), compile and run
Create a new ParentItem in the app (via plus toolbar button in the sidebar).
Add multiple ChildItems to the ParentItem (via plus toolbar button in the content / middle column of the navigation split view).
Press Cmd+Z to undo the last addition.
Post
Replies
Boosts
Views
Activity
I have created a minimum example to demonstrate an issue with observing SwiftData model and UndoManager. This project includes a simple NavigationSplitView, an Item SwiftData model that is being persisted and an enabled UndoManager.
Problem: The SwiftData model Item can be observed as expected. Changing the date in the DetailView works as expected and all related views (ListElementView + DetailView) are updated as expected. When pressing ⌘+Z to undo with the enabled UndoManager, deletions or inserts in the sidebar are visible immediately (and properly observed by ContentView). However, when changing the timestamp and pressing ⌘+Z to undo that change, it is not properly observed and immediately updated in the related views (ListElementView + DetailView).
Further comments:
Undo operation to the model value changes (here: timestamp) are visible in the DetailView when changing sidebar selections
Undo operation to the model value changes (here: timestamp) are visible in the ListElementView when restarting the app
Undo operation to the model value changes (here: timestamp) are are properly observed and immediately visible in the sidebar, when ommiting the ListElementView (no view encapsulation)
Relevant code base:
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
}
}
It seems that the UndoManager does not trigger a redraw of the ContentView through the items query? Is this a bug or a feature?
I am currently refactoring my multi-platform app as it starts growing rapidly. To properly manage my unit tests and keep the app building blocks easy to maintain, I am leveraging frameworks. I was stumbling upon the Safari Web Extension target that my multi-platform app uses.
Is it generally feasible to move the Safari Web Extension code and targets into a dedicated framework? If yes, will it require tricks for code signing before publishing it to the App Store?
Thanks for your insights and thoughts.