1

I'm having an issue with UndoManager which might be an iPadOS bug, but could also be my mistake.

I have a simple test app. One button performs an action, which increments an int by 1 and registers an undo with the system UndoManager. Another button manually calls undoManager.undo(), which undoes the last action until no more actions on the undo stack remain. And the overall app is a first responder with the system, so hitting CMD+Z should also trigger the undoManager to call undo() once per CMD+Z press.

Here's some code.

The UIHostingController subclass that allows becoming first responder:

class FirstResponderHostingController<Content: View>: UIHostingController<Content> {
    override var canBecomeFirstResponder: Bool { true }
}

SceneDelegate, where I pass in the system undo manager:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)

            let model = MyObject(undoManager: window.undoManager)
            let contentView = ContentView(model: model)

            window.rootViewController = FirstResponderHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }
    //...
}

And my SwiftUI view and associated model:

class MyObject: ObservableObject {
    var undoManager: UndoManager?
    var state: Int = 0

    init(undoManager: UndoManager? = nil) {
        self.undoManager = undoManager
    }

    func registerAction() {
        state += 1
        undoManager?.registerUndo(withTarget: self, selector: #selector(performUndo), object: nil)
        print("registered action - state is \(state)")
    }

    @objc func performUndo() {
        state -= 1
        print("performUndo called - state is \(state)")
    }

    func undo() {
        print("undo() called")
        undoManager?.undo()
    }
}

struct ContentView: View {
    @ObservedObject var model: MyObject

    var body: some View {
        VStack(spacing: 16) {
            Button(action: {
                self.model.registerAction()
            }) {
                Text("Register an Action")
            }
            Button(action: {
                self.model.undo()
            }) {
                Text("Manual Undo (Not from Keyboard)")
            }
        }
    }
}

So for example, if I tap "Register Action" twice, and then "Manual Undo" three times, the output is what I expect - the state was incremented twice, then decremented twice, and the last undo() does nothing because the undo stack is empty:

registered action - state is 1
registered action - state is 2
undo() called
performUndo called - state is 1
undo() called
performUndo called - state is 0
undo() called

But if I tap "Register Action" twice, and then hit CMD+Z on the iPad keyboard, I expect the state to be incremented twice and decremented only once, with one call to performUndo, but instead performUndo is still called twice:

registered action - state is 1
registered action - state is 2
performUndo called - state is 1
performUndo called - state is 0

Am I using the system UndoManager incorrectly somehow, or is this a bug?

Update: The above is with Xcode 11.3.1 and iPadOS 13.3. Using the latest Xcode 11.4 beta 2 and iPadOS 13.4 beta 2, the double-undo no longer occurs. Seems fixed!

UberJason
  • 3,063
  • 2
  • 25
  • 50

0 Answers0