I'm trying to implement undo in a SwiftUI app for iOS, but I haven't been able to get the undo gestures to work. Here's a sample that demonstrates the problem:
class Model: ObservableObject {
@Published var active = false
func registerUndo(_ newValue: Bool, in undoManager: UndoManager?) {
let oldValue = active
undoManager?.registerUndo(withTarget: self) { target in
target.active = oldValue
}
active = newValue
}
}
struct UndoTest: View {
@ObservedObject private var model = Model()
@Environment(\.undoManager) var undoManager
var body: some View {
VStack {
Toggle(isOn: Binding<Bool>(
get: { self.model.active },
set: { self.model.registerUndo($0, in: self.undoManager) }
)) {
Text("Toggle")
}
.frame(width: 120)
Button(action: {
self.undoManager?.undo()
}, label: {
Text("Undo")
.foregroundColor(.white)
.padding()
.background(self.undoManager?.canUndo == true ? Color.blue : Color.gray)
})
}
}
}
Switching the toggle around then tapping the undo button works fine. Using the three-finger undo gesture or shaking to undo does nothing. How do you tie in to the system gesture?
Unfortunately, you can't disable three fingers gestures but you can use AssistiveTouch. Please Go to Settings > Accessibility > Touch > AssistiveTouch, then turn on AssistiveTouch. Then you see a button appear onscreen and You can drag the button to any edge of the screen.
Made a mistake while typing in an app? No problem. To quickly undo your last typing command, either double-tap with three fingers, or swipe left with three fingers. If you change your mind and want to redo that removed typing command, swipe right with three fingers.
It appears that the editing gestures require the window to have first responder, and that SwiftUI doesn't set up anything that the UIWindow
wants to pick as first responder by default.
If you subclass UIHostingController
, and in your subclass, you override canBecomeFirstResponder
to return true
, then the UIWindow
will set your controller as first responder by default, which appears sufficient to enable the editing gestures.
I tested the following code on my iPad Pro running iPadOS 13.1 beta 2 (17A5831c). It mostly works. I believe there is an iOS bug, perhaps fixed in a newer beta: when the undo stack is empty, the gestures sometimes don't work (even when a redo action is possible). Switching to the home screen and then back to the test app (without killing the test app) seems to make the editing gestures work again.
import UIKit
import SwiftUI
class MyHostingController<Content: View>: UIHostingController<Content> {
override var canBecomeFirstResponder: Bool { true }
}
class Model: ObservableObject {
init(undoManager: UndoManager) {
self.undoManager = undoManager
}
let undoManager: UndoManager
@Published var active = false {
willSet {
let oldValue = active
undoManager.registerUndo(withTarget: self) { me in
me.active = oldValue
}
}
}
}
struct ContentView: View {
@ObservedObject var model: Model
@Environment(\.undoManager) var undoManager
var body: some View {
VStack {
Toggle("Active", isOn: $model.active)
.frame(width: 120)
HStack {
Button("Undo") {
withAnimation {
self.undoManager?.undo()
}
}.disabled(!(undoManager?.canUndo ?? false))
Button("Redo") {
withAnimation {
self.undoManager?.redo()
}
}.disabled(!(undoManager?.canRedo ?? false))
}
}
}
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: scene)
let model = Model(undoManager: window.undoManager!)
let contentView = ContentView(model: model)
window.rootViewController = MyHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With