Hello,
I'd like to create a macOS app using SwiftUI but I am stuck trying to match the nice idiomatic undo/redo behaviour exhibited by all of Apple's applications I've examined.
Take for example how Xcode's basic undo/redo behavior works (basic, as in ignoring multi-window and what happens when the files are changed). With this undo/redo behaviour, if say we have a scenario in Xcode where there is a:
- Src file 1 in editor tabs 1 and 2
- And a src file 2 in editor tab 3
Then changes :
- Made to file 1 in either tab 1 or 2.
- Can be undone and redone interchangably from when ever either tab 1 or 2 has focus.
- Made to file 2 in tab 3.
- Can only be undone when tab 3 has focus.
- Undo operations for each file regardless of the tab are independent of each other and do not clear or alter the other file's undo stack. i.e. the undo stack is associated with the data.
Contrast that normal macOS undo/redo behavior described above with a mock of this scenario in SwiftUI using its defaults (as shown below using the code attached to the end of the post using either macOS 13.0 Beta/Xcode 14.0 beta 6 or macOS 12 and Xcode 13 ).
Then with the SwiftUI mock, the default undo behaviour is that changes:
- Made to file 1 in either tab 1 or 2.
- Can be undone and redone from any tab with focus including tab 3 (i.e. the tab that is displaying file 2).
- Made to file 2 in tab 3
- Can be undone and redone from any tab with including tabs 1 and 2.
- Undo operations for each file are being conflated with each other in a way that is not the normal behaviour for the macOS platform (suspect the undo stack being used is the one for the window)
In a many view/tab environment, having the user's undo/redo operations mutate data that is both visually distant and unrelated to what they are currently looking at is likely be confusing and is poor usability practice. And I can't see anyway of fixing it.
Any help or suggestions about how to get SwiftUI's undo/redo behaving in a more macOS/usable way - either SwiftUI or wrapping AppKit - much appreciated.
Thanks
import SwiftUI
@main
struct MockCode: App {
@State var srcFile1: String = "src file 1"
@State var srcFile2: String = "src file 2"
var body: some Scene {
WindowGroup {
ContentView(srcFile1: $srcFile1, srcFile2: $srcFile2)
}
}
}
struct ContentView: View {
@Binding var srcFile1: String
@Binding var srcFile2: String
var body: some View {
HStack {
Panel(title: "Tab 1 File 1", text: $srcFile1)
Panel(title: "Tab 2 File 1", text: $srcFile1)
Panel(title: "Tab 3 File 2", text: $srcFile2)
}
}
}
struct Panel: View {
let title: String
@Binding var text: String
var body: some View {
VStack {
Text(title)
TextEditor(text: $text)
}
}
}
Bit of a battle, but have managed to come up with an approach that with a little bit of more polishing can do a reasonable approximation of an idiomatic macOS text editing experience in pure SwiftUI (and is only (of necessity) slightly hacky).
A demo of this approach and an explanation of how it works can be found over on GitHub at https://github.com/shufflingB/swiftui-macos-undoable-texteditor.