Add Return key subview to keyboard

That's an old question (this thread https://developer.apple.com/forums/thread/16375 dates back iOS9 or this other thread https://stackoverflow.com/questions/67249956/uisearchbar-warning-uitexteffectswindow-should-not-become-key-please-file-a-bu), but I've not found a comprehensive answer yet.

I need to add a Return key to a numeric keyboard (BTW, how is it it does not exist in standard ?)

I used to do it using

let keyBoardWindow = UIApplication.shared.windows.last 

to get the keyboard window and then adding the subview (self.returnButton) to it

keyBoardWindow?.addSubview(self.returnButton)

Worked great and still works OK with iOS 15.

But we are told to use connectedScenes instead…

So I adapt code:

      var keyBoardWindow: UIWindow? = nil 
      
      if #available(iOS 13.0, *) {    //  use connectedScenes
             let scenes = UIApplication.shared.connectedScenes
             let windowScene = scenes.first as? UIWindowScene
             if let kbWindow = windowScene?.windows.last  {
                  keyBoardWindow = kbWindow
              }
        } else {
             keyBoardWindow = UIApplication.shared.windows.last 
         }

It runs, but subview does not show.

The problem is that keyboardWindow used to be a UIRemoteKeyboardWindow (the true keyboard window) and now is UITextEffectsWindow, which is not the real keyboardWindow. So adding subview to it adds improperly in the hierarchy.

Note: someone detailed the view hierarchy here: https://developer.apple.com/forums/thread/664547

UIWindow
UITextEffectsWindow
    UIInputWindowController
        UIInputSetContainerView
              UIInputSetHostView
              UIEditingOverlayViewController
                     UIEditingOverlayGestureView

I guess I have to add subview to something else than UITextEffectsWindow (keyBoardWindow), but to what ?

I tried

     keyBoardWindow = kbWindow.superview as? UIWindow

to no avail.

I also tried to debug view hierarchy, but keyboard does not show in debugView.

Answered by Scott in 715600022

I need to add a Return key to a numeric keyboard (BTW, how is it it does not exist in standard ?)

Any chance you can just do this with an inputAccessoryView? That’s a supported API and It Just Works™.

I guess I have to add subview to something else than UITextEffectsWindow (keyBoardWindow), but to what ?

I’m sure you know this already, but (for those in the wider audience who may not know) poking around in a private, undocumented view hierarchy is fragile and unsupported. That way madness lies.

Accepted Answer

I need to add a Return key to a numeric keyboard (BTW, how is it it does not exist in standard ?)

Any chance you can just do this with an inputAccessoryView? That’s a supported API and It Just Works™.

I guess I have to add subview to something else than UITextEffectsWindow (keyBoardWindow), but to what ?

I’m sure you know this already, but (for those in the wider audience who may not know) poking around in a private, undocumented view hierarchy is fragile and unsupported. That way madness lies.

Any chance you can just do this with an inputAccessoryView?

Not really, I want the return / Done key to be at the bottom of keyboard, not as a button at the top.

.

poking around in a private, undocumented view hierarchy is fragile

Yes, I know, that's why I'm looking for an official way to do it…

to get the keyboard window and then adding the subview (self.returnButton) to it

keyBoardWindow?.addSubview(self.returnButton)

Please do not do this. This is not your window to add content to as mentioned by Scott. If you'd like to see something offered by UIKit, or something we offer currently modified, please file a feedback. Doing something like this will only cause you pain when we invariably change the implementation and break you.

      var keyBoardWindow: UIWindow? = nil 
      
      if #available(iOS 13.0, *) {    //  use connectedScenes
             let scenes = UIApplication.shared.connectedScenes
             let windowScene = scenes.first as? UIWindowScene
             if let kbWindow = windowScene?.windows.last  {
                  keyBoardWindow = kbWindow
              }
        } else {
             keyBoardWindow = UIApplication.shared.windows.last 
         }

First, looping over connectedScenes or assuming a specific scene is first or a window is last is not future proof in the slightest. Scenes and windows should be primarily obtained through context: view.window.windowScene etc. If you must loop over scenes, you should always always take care to look at the session.role of the scene, as there are session roles you may not care about.

Second, as you've found out, this code is not going to work. Given that you are interacting with undocumented and private windows, I'm not going to go into more private details as to why. Therefore, the actual solutions available to you are:

  1. Use inputAccessoryView as that's API as Scott mentioned
  2. Perhaps make your own inputView that is designed exactly the way you want
  3. Another API related solution…
  4. File a feedback asking for what you'd like to see but that currently does not exist…

The problem is that keyboardWindow used to be a UIRemoteKeyboardWindow (the true keyboard window) and now is UITextEffectsWindow, which is not the real keyboardWindow.

For what it is worth, this is not a true statement. The keyboard window class is unchanged in iOS 15.

I also tried to debug view hierarchy, but keyboard does not show in debugView.

See comments about this being private and undocumented. There's a reason it isn't present and that should indicate to you that you should not be touching it.

I also tried to debug view hierarchy, but keyboard does not show in debugView.

FWIW, I can see the keyboard window in the view debugger, but it’s in a special separate scene rather than the main scene. And in the memory graph I can find no path to access it. So this proposed hack would be not only tricky and fragile, but seemingly impossible.

File a feedback asking for what you'd like to see but that currently does not exist

Run the keyboard out-of-process like SFSafariViewController so people can’t even begin to try to hack it. 😉

Thanks for all the wise advices, I will give up the adding of a button in the keyboard and use inputAccessoryView instead.

But this is a significant loss of screen estate: 25 more at top in addition to 40 pixels or so already unused at the bottom, …

There is a solution to save screen real estate.

Instead of creating a toolbar, just create a button that is positioned over the top-right corner of the keyboard. It is added to the VC view, not to the keyboard's.

Here is the result:

I used method swizzling to get a hold of the keyboard VC. Hacky as hell of course, but that's what it takes sometimes... I just use it to fix a glitch/bug with the keyboard that Apple never bothered to fix for years...

@Janneman84 could you detail how you implemented it ?

Add this somewhere:

extension UIViewController {

    static func swizzleLifecycleMethods() {
        //this makes sure it can only swizzle once
        _ = self.actuallySwizzleLifecycleMethods
    }   

    private static let actuallySwizzleLifecycleMethods: Void = {
        let originalVdlMethod = class_getInstanceMethod(UIViewController.self, #selector(viewDidLoad))
        let swizzledVdlMethod = class_getInstanceMethod(UIViewController.self, #selector(swizzledViewDidLoad))
        method_exchangeImplementations(originalVdlMethod!, swizzledVdlMethod!)      

        let originalVwaMethod = class_getInstanceMethod(UIViewController.self, #selector(viewWillAppear(_:)))
        let swizzledVwaMethod = class_getInstanceMethod(UIViewController.self, #selector(swizzledViewWillAppear(_:)))
        method_exchangeImplementations(originalVwaMethod!, swizzledVwaMethod!)

        let originalVdaMethod = class_getInstanceMethod(UIViewController.self, #selector(viewDidAppear(_:)))
        let swizzledVdaMethod = class_getInstanceMethod(UIViewController.self, #selector(swizzledViewDidAppear(_:)))
        method_exchangeImplementations(originalVdaMethod!, swizzledVdaMethod!)

        let originalVddMethod = class_getInstanceMethod(UIViewController.self, #selector(viewDidDisappear(_:)))
        let swizzledVddMethod = class_getInstanceMethod(UIViewController.self, #selector(swizzledViewDidDisappear(_:)))
        method_exchangeImplementations(originalVddMethod!, swizzledVddMethod!)
    }()

    @objc private func swizzledViewDidLoad() -> Void {
        swizzledViewDidLoad() //run original implementation
        print("swizzledViewDidLoad \(self)")
    }
   
    @objc private func swizzledViewWillAppear(_ animated: Bool) -> Void {
        swizzledViewWillAppear(animated) //run original implementation
        print("swizzledViewWillAppear \(self)")

        if type(of: self).description() == "UICompatibilityInputViewController" {
            self.view.printSubViews()
            if (self.view?.subviews.count == 0) {
                self.view?.backgroundColor = .red
            }
        }
    }

    @objc private func swizzledViewDidAppear(_ animated: Bool) -> Void {
        swizzledViewDidAppear(animated) //run original implementation
        print("swizzledViewDidAppear \(self)")
    }

    @objc private func swizzledViewDidDisappear(_ animated: Bool) -> Void {
        swizzledViewDidDisappear(animated) //run original implementation
        print("swizzledViewDidDisappear \(self)")

        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
            if let self {
                print("swizzled VC stil in memory after disappearing for 1s: \(self)")
            }
        }
    }
}

extension UIView {
    /**
    Recursively prints all subviews to console, indented according to level for easy view tree assessment.

    - Parameter level: initial indentation level, default is 0
    */
    func printSubViews(level : UInt = 0) {

        var tabs = ""
        for _ in 0..<level {
            tabs += "\t"
        }

        //do your print() here of whatever you'd like to know of each view:
        print("\(tabs)\(self)")

        for subview in self.subviews {
            subview.printSubViews(level: level+1)
        }
    }
}

Then call UIViewController.swizzleLifecycleMethods() once somewhere to activate the swizzling, I suggest in didFinishLaunchingWithOptions. All keyboards should look red now :). It should print lifecycle events of all VCs to the console, including system ones like the keyboard.

Add Return key subview to keyboard
 
 
Q