36

What I'm trying to do is to create something similar to the "find on page" search function in Safari on iPad.

I'm using a UIToolbar with some items in it and attached it to the keyboard by setting it as an inputAccessoryView on the UITextField. Works like a charm, but there is one thing I can't figure out. In Safari, when you search for something, the keyboard disappears but the tool bar remains on the bottom of the screen.

Does anyone have a clue on how to accomplish this? The only solution I can think of is to respond to a keyboard dismissed event and then pull out the UIToolBar and create a custom animation that moves it to the bottom of the screen. But this is hacky. I am looking for a more elegant solution. Something that can make me decide what to do with the input accessory view when the keyboard gets dismissed.

jscs
  • 62,161
  • 12
  • 145
  • 186
Tom van Zummeren
  • 8,729
  • 12
  • 48
  • 59

5 Answers5

60

It's done like this:

Assign your UIToolbar to a property in your view controller:

@property (strong, nonatomic) UIToolbar *inputAccessoryToolbar;

In your top view controller, add these methods:

- (BOOL)canBecomeFirstResponder{

    return YES;

}

- (UIView *)inputAccessoryView{

    return self.inputAccessoryToolbar;

}

And then (optionally, as it usually shouldn't be necessary), whenever the keyboard gets hidden, just call:

[self becomeFirstResponder];

That way, your inputAccessoryToolbar will be both your view controller's and your text view's input accessory view.

arik
  • 23,480
  • 35
  • 91
  • 147
  • 1
    It doesn't seem to work on iOS8, i get UIViewControllerHierarchyInconsistency, any ideas? – MegaManX Feb 12 '15 at 16:31
  • 2
    It does work on iOS 8 for me, so I'm probably gonna need a code sample. – arik Feb 12 '15 at 16:32
  • 1
    Just made new sample project, added view at a bottom via storyboard, made outlet to it, and returned that view in inputAccessoryView method... I get the same error like in my project. – MegaManX Feb 13 '15 at 09:25
  • I opened new question for iOS8, you can find it here http://stackoverflow.com/questions/28496409/leaving-inputaccessoryview-visible-after-keyboard-is-dismissed-ios8 – MegaManX Feb 13 '15 at 09:35
  • This should be accepted answer :) It is working on iOS8 – Ahmad Raza Feb 15 '15 at 18:44
  • Can i use a Outlet UIView to return the inputAccessoryView? – Gaby Fitcal Jan 13 '16 at 14:38
  • This is perfect! – Baran Emre Sep 28 '17 at 12:53
  • This is the bomb! I was just about to start on a contrived way consisting of invisible accessory views, KVO on the frame and updating constraints on the fly. And then I stumbled upon this. Gold. – Sergiu Todirascu Jun 10 '18 at 09:21
  • To get rid of the `UIViewControllerHierarchyInconsistency` problem: the `inputAccessoryToolbar` (or yet any kind of `UIView`) can reside in the storyboard (e.g. inside your UIViewController's scene), but *must not have the view controller's view as any of its superviews*. – thetrutz Apr 10 '19 at 13:32
7

I've ended up with UIToolBar that is not assigned as input accessory view, and slide up and down on UIKeyboardWillShowNotification / UIKeyboardWillHideNotification

anticyclope
  • 1,539
  • 1
  • 10
  • 26
5

Update to Swift 4, based on prior answers. If you add toolbar via storyboards you can do this

class ViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!
    @IBOutlet var toolbar: UIToolbar!

    override var canBecomeFirstResponder: Bool {
        get {
            return true
        }
    }

    override var inputAccessoryView: UIView {
        get {
            return self.toolbar
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        textField.inputAccessoryView = toolbar
    }
}

In this case, whenever text field resigns first responder, it defaults first responder to main view. Keep in mind, you might want to explicitly resign first responder, and set main view as first responder if there are multiple UI elements and first responder defaults to undesired view after resignation.

esesmuedgars
  • 118
  • 1
  • 6
4

Adding to @arik's answer, here is the Swift version:

class ViewController: UIViewController {

  @IBOutlet var textField: UITextField!      

  // Input Accessory View
  private var inputAccessoryToolbar: UIToolBar?
  override func canBecomeFirstResponder() -> Bool {
    return true
  }
  override var inputAccessoryView: UIView? {
    return inputAccessoryToolbar
  }

  override func viewDidLoad() {
    super.viewDidLoad()
    inputAccessoryToolbar = UIToolbar(frame: CGRectMake(0, 0, view.frame.size.width, 50))
    textField.inputAccessoryView = inputAccessoryToolbar
  }

  // UITextFieldDelegate
  func textFieldShouldReturn(textField: UITextField) -> Bool {
    becomeFirstResponder()
    return true
  }
}

Thanks for the clean solution!

Alex Koshy
  • 1,533
  • 14
  • 16
  • In Swift 3/4 , `canBecomeFirstResponder` and `inputAccessoryView` are now properties instead of functions/methods. – AnBisw Oct 19 '17 at 03:59
1

You may also need to work around the bug with the inputAccessoryView not respecting the safe area margins and thus not making room for home indicator thing on iPhone X: iPhone X how to handle View Controller inputAccessoryView?

I found the easiest solution when you have a UIToolbar from a xib and you are also using that UIToolbar as the inputAccessoryView of a text field is to embed the toolbar in a UIView when you return it from your overridden inputAccessoryView, and make the containing UIView taller by the safeAreaInsets.bottom. (Other solutions suggest constraining the bottom of the toolbar to the safe area in a subclass, but this leads to constraint conflicts and also means the area under the toolbar is the wrong colour.) However, you have to also bear in mind that the text field can have focus even when there is no keyboard on the screen (for instance if there is an external keyboard), so you need to change the inputAccessoryView of the text view to this toolbar-within-a-UIView in that case as well. In fact it will probably make things simpler to just always use the containing view and adjust the size of it appropriately. Anyway, here's my override of inputAccessoryView:

    override var inputAccessoryView: UIView? {

    if toolbarContainerView == nil {

        let frame=CGRect(x: toolBar.frame.minX, y: toolBar.frame.minY, width: toolbar.frame.width, height: toolBar.frame.height+view.safeAreaInsets.bottom)

        toolbarContainerView = UIView(frame: frame)

    }

    if (toolbar.superview != toolbarContainerView) {

        //this is set to false when the toolbar is used above the keyboard without the container view

        //we need to set it to true again or else the toolbar will appear at the very top of the window instead of the bottom if the keyboard has previously been shown.

        toolbar.translatesAutoresizingMaskIntoConstraints=true

        toolbarContainerView?.addSubview(toolbar)

    }

    return toolbarContainerView

}

It would probably be a good idea to override viewSafeAreaInsetsDidChange to adjust the size of toolbarContainerView in that case, too.

Angela
  • 109
  • 1
  • 11