swiftkeyboard-shortcutsipadosuikeycommandmulti-scene

How to determine the source of unwanted iPadOS external keyboard shortcuts in a multiple window app?


At the time of writing, I’m developing with Xcode 14.2 and iPadOS 16.3.1.

I am updating an existing app to support multiple windows/scenes for iPadOS. The main scene uses UIKit and the child scenes use mainly SwiftUI. I turned my attention to testing external keyboard shortcuts, which were already implemented from previous code in the app, i.e: holding down the CMD key on the external keyboard or keyboard of computer running the device simulator.

Desired shortcuts

These shortcut options are implemented from a view in the main scene that returns values via:

override var keyCommands: [UIKeyCommand]?

However, sometimes I see unwanted/unnecessary Edit options that I have not implemented explicitly in the project:

unwanted Edit options

Occasionally, this unwanted Edit option seems to be present after opening the app. It always seems to be present after opening a child scene. Even when completely closing a child scene (no longer present in UIApplication.shared.openSessions / connectedScenes), the Edit aspect is still shown.

There’s nothing obvious in the code of the other scenes to which I can attribute the unwanted options. Any ideas how to add tracing to determine the source of the Edit option?

Update: I have ruled out opening child scenes as being the cause. It happens also when the window resizes from rotating the device. Hence, something in the main scene must be a cause. Adding test code to recursively inspect all subviews did not find anything odd such as two views set to being firstResponder. The Edit options look like something a text control might return but I haven't spotted anything yet that might provide such a source.


Solution

  • I found that the UIApplication was responding true to an internal _handleLegacyEmojiKeyboardShortcut: selector when its canPerformAction(...) method was invoked. I don't know why this occurs but it seems to start responding true be after the window has resized (e.g. rotating the device).

    I discovered this with the following trace code to the view where the keyCommands are overridden:

    func processResponders(_ action: Selector, withSender sender: Any?, next: UIResponder?) {
        if let next {
            let result = next.canPerformAction(action, withSender: sender)
            print("******** canPerformAction \(next) \(action) \(String(describing: sender)) \(result)")
            processResponders(action, withSender: sender, next: next.next)
        }
    }
    
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        
        let result = super.canPerformAction(action, withSender: sender)
        print("******** canPerformAction \(action) \(String(describing: sender)) \(result)")
        processResponders(action, withSender: sender, next: next)
        return result
    }
    

    The solution I chose was to define a custom UIApplication with the following to stop the application class returning true:

    import UIKit
    
    class CustomApplication: UIApplication {
        
        override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
            guard sender is UIKeyCommand == false else { return false }
            return super.canPerformAction(action, withSender: sender)
        }
    
    }