Skip to content →

Month: October 2016

Text Field Advancing Protocol

In Keyboard Responsive View Controller, I discussed a demo project that presented a UIViewController extension for automatically moving the active text field into view when obscured by the keyboard. Not discussed there was functionality included in that project for advancing the cursor to the next text field when the user taps the Return key. I discuss it now.

By default, when the user taps the Return key on the iOS keyboard (in this project configured as a Next or Done button), nothing happens. By conforming your UIViewController to the TextFieldAdvancing protocol, your controller gains a text field auto-advance functionality. As a result, when the user taps Return, the focus automatically moves to the next field in the sequence. Optionally, if it’s the last field on the view, it will instead segue to a new scene.

This protocol does require your controller be registered as a delegate of each text field, which is easiest done via the storyboard. Just control-drag from the text field to the view controller button at the top of the scene, and select the “delegate” outlet. You can also do this programmatically with myTextField.delegate = self (this functionality is included in the Keyboard Responsive View Controller extension).

@objc protocol TextFieldAdvancing: UITextFieldDelegate {
  var textFieldSegueIdentifier: String? { get }
}

extension UIViewController {

  /// In response to the user tapping the Return key in a text field, calls upon `advanceFirstResponder(from:)`.
  func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    advanceFirstResponder(from: textField)
    return true
  }

  /// Advances first responder to the next text field in the `tag` sequence. For the last text field in the sequence, advances to the next scene via the segue given by `textFieldSegueIdentifier`.
  func advanceFirstResponder(from textField: UITextField) {
    guard let vc = self as? TextFieldAdvancing else { return }
    if let nextTextField = view.viewWithTag(textField.tag + 1) {
      nextTextField.becomeFirstResponder()
    } else {
      if let textFieldSegueIdentifier = vc.textFieldSegueIdentifier {
        performSegue(withIdentifier: textFieldSegueIdentifier, sender: nil)
      }
    }
  }
}

The @objc designation on line 1 is required in order for the Swift protocol’s UITextFieldDelegate method textFieldShouldReturn(_:) to be seen and called by the Objective-C UITextField. Without that, tapping Return has no effect.

Turning to the extension, in a perfect Swift world, this would be a protocol extension, with line 5 reading:

extension TextFieldAdvancing where Self: UIViewController {

However, the UITextFieldDelegate calls are similarly never made.  While it runs, the compiler warns:

Non-@objc method textFieldShouldReturn does not satisfy optional requirement of @objc protocol UITextFieldDelegate.

Matthew Seaman provides a good discussion of this problem, summing up the issue so:

@objc functions may not currently be in protocol extensions. You could create a base class instead, though that’s not an ideal solution.

No, not ideal. Using a base class effectively eliminates the utility of using a protocol in the first place. And because we can’t designate that this extension is of TextFieldAdvancing, we must explicitly test for conformance on line 15.

Let that not detract from the fact, however, that this protocol and extension do work. If you want the user to be able to advance automatically through text fields managed by your view controller, simply conform to TextFieldAdvancing, like so:

class MyViewController: UIViewController, TextFieldAdvancing {
  var textFieldSegueIdentifier: String? = "mySegue"
}

And don’t forget to assign the controller as the delegate of each text field!

Leave a Comment

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