Search code examples
iosswiftuikitmac-catalyst

Override UITextView's CMD + Z key command


Is it possible to override UITextView's handling of cmd + z and cmd + shift + z ?

I've tried to

  • override the keyCommand property, but the selectors are never called..
  • override the undoManager, this doesn't help either
class CustomTextView: UITextView {

    override var keyCommands: [UIKeyCommand]? {
       [
            // cmd + z (doesn't work)
            UIKeyCommand(input: "z", modifierFlags: [.command], action: #selector(undo)),
                
            // cmd + shift + z  (doesn't work)
            UIKeyCommand(input: "z", modifierFlags: [.command, .shift], action: #selector(redo)),
                
            // z (works)
            UIKeyCommand(input: "z", modifierFlags: [], action: #selector(z)),
        ]
    }
     
    // this doesn't help   
    override var undoManager: UndoManager? { return nil }
        






    // undo
    @objc private func undo() {
        print("undo")
    }

    // redo
    @objc private func redo() {
        print("redo")
    }

    // z
    @objc private func z() {
        print("z")
    }   
}

Solution

  • You can do this with a UITextView+UIResponder.swift extension:

    import UIKit
    
    extension UITextView {
        static var myUndoManager = UndoManager()
    
        fileprivate func handleUndo () { ... }
        fileprivate func handleRedo () { ... }
    
        override var undoManager : UndoManager? {
            return Self.myUndoManager
        }
    
        override func pressesBegan (
            _ presses: Set<UIPress>
          , with event: UIPressesEvent?
        )
        {
            for press in presses {
                guard
                    let key = press.key
                  , key.charactersIgnoringModifiers == "z"
                else { continue }
    
                switch key.modifierFlags {
                    case .command:
                        handleUndo()
                    case [ .command, .shift]:
                        handleRedo()
                    default:
                        break
                }
            }
            super.pressesBegan (
                presses
              , with: event
            )
        }
    }
    

    Relevant Apple References:

    UIResponder::func pressesBegan(_:) https://developer.apple.com/documentation/uikit/uiresponder

    Keyboards & Input::class UIKey https://developer.apple.com/documentation/uikit/uikey

    See Also: https://stackoverflow.com/questions/tagged/uiresponder

    Why won't overriding keyCommands work?

    UITextView is opaque, so I can't see for myself , but I think it overrides pressesBegan(_:with:) and does not call super for any key-press it handles. Without that call to super, UIPressesEvent events never propagate up the responder chain; keyCommands will never see them.