Post

Replies

Boosts

Views

Activity

SwiftUI macOS undo/redo behavior - idiomatic TextEditor how?
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) } } }
1
0
1k
Aug ’22
Expected behaviour iOS14 vs iOS15 - Bound text in TextField goes out of sync with source of truth value on iOS15 Xcode Beta 2
Hi, So in iOS14, I've been using a code pattern to listen for changes in TextField's text bound value and to reject/drop unwanted changes and values. In iOS15, this pattern no longer seems to work , but I've not found others complaining about it, so I'm now wondering is iOS15's behaviour a bug, or just the expected behavior? A simplified example of this type of code pattern is shown below which when run on: iOS14 effectively disables the use of the "3" button. iOS15 Xcode Beta 2 does not, instead even though source of truth "textFieldText" reflects the expected value, the TextField displays what is typed regardless. I've put FB9290496 , thoughts etc welcome. Thanks import SwiftUI class No3ViewModel: ObservableObject { // Xcode 13 beta 2 - iOS14 prevents the entry and display of the digit "3" in the TextField // that uses it. While in iOS15 it does not (even when the backing store has the correct value). private var textFieldTextBacking: String = "" var textFieldText: String { get { textFieldTextBacking } set { /// Prevent infinite loops guard newValue != textFieldTextBacking else { return } /// Prevent setting input that contrains a "3" and force a rething of anythig that has speculatively done that, guard !newValue.contains("3") else { objectWillChange.send() return } textFieldTextBacking = newValue objectWillChange.send() } } } struct ContentView: View { @StateObject var vm = No3ViewModel() var label: String { vm.textFieldText == "" ? "Enter anything except 3" : "not set"} var body: some View { return VStack { Text("I reject the number 3 - my backing value is = \"\(vm.textFieldText)\"") if #available(iOS 15.0, *) { TextField(label, text: $vm.textFieldText, prompt: Text("Bum")) .keyboardType(.decimalPad) .textFieldStyle(RoundedBorderTextFieldStyle()) } else { // Fallback on earlier versions TextField(label, text: $vm.textFieldText) .keyboardType(.decimalPad) .textFieldStyle(RoundedBorderTextFieldStyle()) } } .padding([.leading, .trailing], 16) } }
7
2
3.7k
Jul ’21