Skip to content →

Tag: UI

Keyboard Responsive View Controller

keyboardresponsivedemoHere’s a quick-n-easy UIViewController extension that will make any view with UITextFields on it responsive to the keyboard, moving text fields into view when they would otherwise be obscured by it…without requiring scroll views or other storyboard configurations.

I put this together after implementing the Apple approach using scroll views: Moving Content that is Located Under the Keyboard. I found the overhead associated with reconfiguring my existing project views with scroll views significant and the results inconsistent between views. This approach requires no changes to existing views, and needs but one line of code in your controller to enable the feature (plus a couple “optional” ones).

You will find the Swift 3 demo extension project on GitHub in the extension branch (there’s another branch we’ll get to momentarily).

Note, in the real world you’d do better to implement this particular layout in a UITableViewController; I use it here simply for demonstration purposes. This technique is intended more for content that doesn’t lend itself to a tabular or scrolling view. (Also, for simplicity, I forego the universal layout machinations; this one will look decent on an iPhone 6/7 or 6/7 Plus.)

Here’s the extension.

extension UIViewController {

  /// Returns the text field currently identified as the view's first responder, or `nil` if there is no such first responder.
  func activeTextField(within view: UIView?) -> UITextField? {
    guard let view = view else { return nil }
    if view.isFirstResponder {
      return view as? UITextField
    }
    for view in view.subviews {
      if let activeTextField = activeTextField(within: view) {
        return activeTextField
      }
    }
    return nil
  }


  /// In response to keyboard presentation, animates the active text field into view if obscured by the keyboard.
  func keyboardWillShow(_ notification: NSNotification) {

    // The distance between the bottom of the text field and the top of the keyboard
    let gap: CGFloat = 20

    if let activeTextField = activeTextField(within: view),
      let keyboardFrame = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {

      // Calculate delta between upper boundary of keyboard and lower boundary of text field
      let textFieldFrame = activeTextField.superview!.convert(activeTextField.frame, to: view)
      let textFieldBound = textFieldFrame.origin.y + textFieldFrame.size.height + gap
      let keyboardBound = keyboardFrame.origin.y
      let viewShift = min(keyboardBound - textFieldBound, 0) // Don't shift if keyboard doesn't cross text field boundary

      // Shift the view
      var viewFrame = view.frame
      viewFrame.origin.y += viewShift - viewFrame.origin.y // Account for previous shift
      UIView.animate(withDuration: 0.5) {
        self.view.frame = viewFrame
      }
    }
  }

  /// In response to keyboard dismissal, animates the view back to its original position.
  func keyboardWillHide(_ notification: NSNotification) {
    var vwFrame = view.frame
    vwFrame.origin.y = 0
    UIView.animate(withDuration: 0.5) {
      self.view.frame = vwFrame
    }
  }

  /// Enables keyboard responsiveness to text fields, animating the view such that the currently active text field is not obscured by the keyboard. Call this method from `viewDidAppear(_:)`.
  func activateKeyboardResponsiveTextFields() {
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
  }

  /// Disables keyboard responsiveness to text fields. Call this method from `viewWillDisappear()`.
  func deactivateKeyboardResponsiveTextFields() {
    NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillShow, object: nil)
    NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillHide, object: nil)
  }
}

The view controllers you wish to take advantage of this behavior need to call upon those last two methods, as follows:

override func viewDidAppear(_ animated: Bool) {
  activateKeyboardResponsiveTextFields()
}

override func viewWillDisappear(_ animated: Bool) {
  view.endEditing(true)
  deactivateKeyboardResponsiveTextFields()
}

The call to endEditing() on line 6 is not strictly necessary, but I found overcame some less-than-premium keyboard presentation behavior when unwinding to a controller that had previously segued with the keyboard visible.

Note that in the extension itself, beginning on line 4, we cannot obtain the activeTextField the same way Apple does in their example code, that is, by using the UITextFieldDelegate methods textFieldDidBeginEditing(_:) and textFieldDidEndEditing(_:). This is because they use a stored property to capture the active field. Thus the extension must find it programmatically by recursively traversing the view’s subviews, looking for the one that isFirstResponder.

This activeTextField(within:) method is called once each time the keyboard shows, which actually happens each time a field becomes the first responder, such as when the focus moves from one text field to the next, even if the keyboard is already present. This process happens so quickly within the run loop, however (even with this activeTextField(within:) traversal), that the keyboard doesn’t actually dismiss and reappear.

Where could you go from here?

While this extension is arguably Swifty and lightweight, there is some overhead we carry into each view controller taking advantage of it. We can eliminate that overhead by converting the extension into a subclass of UIViewController. While perhaps non-Swifty (classes are so heavyyy), it does have these benefits:

  • Replaces what amounts to a computed activeTextField property with a stored property (however negligible that processing requirement may be).
  • Eliminates the explicit calls to (de) activateKeyboardResponsiveTextFields(). Instead, one must only subclass KeyboardResponsiveViewController.

Take a look at the subclass branch of the repo for this implementation.

Leave a Comment

Container Views

I recently developed a passcode view with core functionality that allows a user to tap in a 6-digit passcode. I then added functionality to support two different modes of operation: new passcode entry and existing passcode entry. I had a single view controller that managed these two modes of operation. The mode was ultimately determined by two different calling view controllers that segued to it.

And it was just too complicated for my tastes.

So I set out this weekend to break it up. I ended up with three controllers: one for the basic passcode-entry functionality, a second for the new-passcode additions, and a third for the existing-passcode additions.

But how to handle this in Interface Builder? Thanks to Xcode 7 and the help of Mike Woelmer, container views was the answer.

Screen Shot 2016-03-05 at 10.29.51 PM
Define Once, Use Twice

As illustrated above, I simply added two additional view controllers to the storyboard, one for each mode of operation, set their Custom Class to one of the new UIViewController subclasses I’d just created, and filled their view with a Container View. Then I embedded the single passcode view controller into each by control-dragging from the container to the passcode controller.

Leave a Comment