Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI and the three-finger undo gesture

Tags:

undo

ios

swiftui

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?

like image 609
smr Avatar asked Sep 14 '19 01:09

smr


People also ask

How do you get rid of the three finger gesture on iOS 13?

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.

How do I undo in iOS 15?

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.


1 Answers

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()
    }
}
like image 155
rob mayoff Avatar answered Sep 26 '22 01:09

rob mayoff