Search code examples
iosuitextfieldfirst-responderpresentviewcontroller

presenting a modal view controller while the keyboard is active


So I basically have a form, consisting of several text fields. The user types into the fields as usual. But the user also has the option of double-tapping a text field, which presents a modal view controller, allowing the user to choose from a number of options relating to that field.

Can I somehow present the modal "over" the keyboard, such that when it is dismissed, the keyboard is still active for the field that had been first responder before I presented the modal?

Right now, the keyboard dismisses while the modal appears, and reappears as the modal is dismissed. It looks clunky to me, and distracting. Would love to streamline it, and reduce the amount of animation onscreen.


Solution

  • Edit: I've updated this answer for iOS 12 and Swift. The revised example project (containing new Swift and updated Objective-C implementations) is here.


    You can create a new UIWindow and place that over the default window while hiding the keyboard's window.


    Animated example of overlaying a new UIWindow over the existing one


    I have an example project on Github here, but the basic process is below.

    • Create a new UIViewController class for your modal view. I called mine OverlayViewController. Set up the corresponding view as you wish. Per your question you need to pass back some options, so I made a delegate protocol OverlayViewController and will make the primary window's root view controller (class ViewController) our delegate.
    protocol OverlayViewControllerDelegate: class {
      func optionChosen(option: YourOptionsEnum)
    }
    
    • Add some supporting properties to our original view controller.
    class ViewController: UIViewController {
      /// The text field that responds to a double-tap.
      @IBOutlet private weak var firstField: UITextField!
      /// A simple label that shows we received a message back from the overlay.
      @IBOutlet private weak var label: UILabel!
      /// The window that will appear over our existing one.
      private var overlayWindow: UIWindow?
    
    • Add a UITapGestureRecognizer to your UITextField.
    override func viewDidLoad() {
      super.viewDidLoad()
    
      // Set up gesture recognizer
      let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap))
      doubleTapRecognizer.numberOfTapsRequired = 2
      doubleTapRecognizer.delegate = self
    
      firstField.addGestureRecognizer(doubleTapRecognizer)
    
      firstField.becomeFirstResponder()
    }
    
    • UITextField has a built-in gesture recognizer, so we need to allow multiple UIGestureRecognizers to operate simultaneously.
    extension ViewController: UIGestureRecognizerDelegate {
      // Our gesture recognizer clashes with UITextField's.
      // Need to allow both to work simultaneously.
      func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
                             shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
      }
    }
    
    • This is the interesting part. When the gesture recognizer is triggered, create the new UIWindow, assign your OverlayViewController as the root view controller, and show it. Note that we set the window level to UIWindowLevelAlert so it will appear in front. However, the keyboard will still be in front despite the alert window level, so we have to manually hide its window, too.

    It is important to not set the new UIWindow as key or to change the first responder from the UITextField or the keyboard will be dismissed.

    Previously (before iOS 10?) we could get away with overlayWindow.makeKeyAndVisible(), but now setting it as key will dismiss the keyboard. Also, the keyboard's window now has a non-standard UIWindow.Level value that is in front of every publicly defined value. I've worked around that by finding the keyboard's window in the hierarchy and hiding it instead.

    @objc func handleDoubleTap() {
        // Prepare the overlay window
        guard let overlayFrame = view?.window?.frame else { return }
        overlayWindow = UIWindow(frame: overlayFrame)
        overlayWindow?.windowLevel = .alert
        let overlayVC = OverlayViewController.init(nibName: "OverlayViewController", bundle: nil)
        overlayWindow?.rootViewController = overlayVC
        overlayVC.delegate = self
    
        // The keyboard's window always appears to be the last in the hierarchy.
        let keyboardWindow = UIApplication.shared.windows.last
        keyboardWindow?.isHidden = true
    }
    
    • The overlay window is now the original window. The user can now select whatever options you built into the overlay view. After your user selects an option, your delegate should take whatever action you intend and then dismiss the overlay window and show the keyboard again.
    func optionChosen(option: YourOptionsEnum) {
      // Your code goes here. Take action based on the option chosen.
      // ...
    
      // Dismiss the overlay and show the keyboard
      overlayWindow = nil;
      UIApplication.shared.windows.last?.isHidden = false
    }
    
    • The overlay window should disappear, and your original window should appear with the keyboard in the same position as before.