0

I'm trying to implement an input accessory view that works just like Messages app in iOS. I've searched almost every SO questions regarding this topic, but couldn't find the solution that worked for me.

Here is the minimal reproducible code I created, referring to this SO post.

import UIKit

class TestViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        becomeFirstResponder()  // seems unnecessary
    }
    
    override var inputAccessoryView: UIToolbar {
        return self.keyboardAccessory
    }
    
    override var canBecomeFirstResponder: Bool {
        return true
    }

    var textView: UITextView = {
        let view = UITextView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .yellow
        return view
    }()
    
    lazy var keyboardAccessory: UIToolbar = {
        let inputAccessory = UIToolbar(frame: .init(x: 0, y: 0, width: 0, height: 100))
        inputAccessory.addSubview(textView)
        NSLayoutConstraint.activate([
            textView.centerXAnchor.constraint(equalTo: inputAccessory.centerXAnchor),
            textView.centerYAnchor.constraint(equalTo: inputAccessory.centerYAnchor),
            textView.widthAnchor.constraint(equalToConstant: 200),
            textView.heightAnchor.constraint(equalToConstant: 50)
        ])
        inputAccessory.backgroundColor = .gray
        return inputAccessory
    }()
}

Every article I've seen suggests overriding inputAccessoryView and canBecomeFirstResponder, and that's it. However, the keyboard does not appear until I tap the textView.

Can anyone let me know what I'm missing?

Edit

As @DonMag pointed out, Messages app in iOS does not show keyboard automatically. Please consider following UI in Facebook instead.

When I press the comment button, it pushes to another view controller while popping up the keyboard. The transition effect doesn't have to be exactly the same, but I want the keyboard become fully loaded within presented view controller, as if I called becomeFirstResponder() in viewDidLoad.

enter image description here

shinhong
  • 406
  • 4
  • 13

2 Answers2

1

Actually when you override canBecomeFirstResponder the keyboard is appear just under the view , thats why you only see the accessory view bottom side of the view . You can basically try this with adding notification to your controller like

 override func viewDidLoad() {
    super.viewDidLoad()

    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name:UIResponder.keyboardWillShowNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name:UIResponder.keyboardWillHideNotification, object: nil)

}

 @objc func keyboardWillShow(notification:NSNotification) {

    let userInfo = notification.userInfo!
    let keyboardFrame:CGRect = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
    print(keyboardFrame)
    print(self.view.frame)
}

When you run the project , you gonna see the keyboardWillShow notification is hired.(If you delete overiride canBecomeFirstResponder it won't )

And when you print keyboard and view frame , you gonna notice to keyboard's y position is equal to view's frame height . That means keyboards want to show us only its accessoryView .

So , you need to hired textView.becomeFirstResponder() in keyboardWillShow notification

@objc func keyboardWillShow(notification:NSNotification) {

    let userInfo = notification.userInfo!
    let keyboardFrame:CGRect = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
    textView.becomeFirstResponder()
}

Do not forget to deinit notification when controller deinit

  deinit {
    NotificationCenter.default.removeObserver(self)
}
Omer Tekbiyik
  • 4,255
  • 1
  • 15
  • 27
  • Thanks for the explanation. Then what's the reason `textView.becomFirstResponder()` not working in `viewDidLoad`? I tried in `viewDidLoad`, `viewWillAppear`, `viewWillLayoutSubViews`, `viewDidLayoutSubviews`, but only after `viewWillLayoutSubViews` did it start working (with some glitches). – shinhong Jan 03 '22 at 17:39
  • 1
    because of the life cycle. `viewdidload` is hired before the `override var inputAccessoryView` , thats why adding becomeFirstresponder to textfield (that is not added to view hierarcy) is meaningless . if we come ` viewWillLayoutSubViews` , we can not guarantee how many times this function calling. Maybe 1 time , it called after ` textifeld` added to hieararcy thats why it works. – Omer Tekbiyik Jan 03 '22 at 17:45
  • I see. But I wonder if adding `NotificationCenter` is the best practice here. When pushing to `TestViewController` from another VC, it gives unpleasant transition effect. I just wish I could use `becomeFirstResponder` in `viewDidLoad` as any other textViews :( – shinhong Jan 03 '22 at 18:07
  • If this page must have a keyboard , you dont declare a lazy keyboardAccessory. just add your textfield and its accesory view into viewdidload then call becomeFirstResponder in viewdidload. It gonna fix your problem . And there will be no need override this methods. – Omer Tekbiyik Jan 03 '22 at 18:14
  • Could you elaborate on how to add input accessory view without overriding? To be more specific on my requirements, whether to show the keyboard on view load depends on how the user pushes the view controller(i.e. which button user tapped). – shinhong Jan 03 '22 at 18:26
1

If you want the text view to become active, and the keyboard to show, as soon as the view appears, use:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    textView.becomeFirstResponder()
}

If you want the text view to be visible at the bottom, and become active / show the keyboard when the textview is tapped, take a look at this answer:

https://stackoverflow.com/a/61508928/6257435


Edit

If you want to push a view controller onto the navigation stack, and have the keyboard showing with a custom input accessory view, containing a text view, and give it the focus...

Add a hidden text field to the controller. In viewDidLoad tell that text field to use the custom input accessory view and tell it to become first responder.

Then, in viewDidAppear tell the text view in the custom input accessory view to become the first responder:

class TestViewController: UIViewController {
    
    var hiddenTF = UITextField()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        // set the text field to hidden
        hiddenTF.isHidden = true
        // add it to the view
        view.addSubview(hiddenTF)

        // tell hidden text field to use custom input accessory view
        hiddenTF.inputAccessoryView = keyboardAccessory
        
        // tell it to become first responder
        hiddenTF.becomeFirstResponder()
    }
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // tell the textView (in the custom input accessory view)
        //  to become first responder
        textView.becomeFirstResponder()
    }
    
    var textView: UITextView = {
        let view = UITextView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .yellow
        return view
    }()
    
    lazy var keyboardAccessory: UIToolbar = {
        let inputAccessory = UIToolbar(frame: .init(x: 0, y: 0, width: 0, height: 100))
        inputAccessory.addSubview(textView)
        NSLayoutConstraint.activate([
            textView.centerXAnchor.constraint(equalTo: inputAccessory.centerXAnchor),
            textView.centerYAnchor.constraint(equalTo: inputAccessory.centerYAnchor),
            textView.widthAnchor.constraint(equalToConstant: 200),
            textView.heightAnchor.constraint(equalToConstant: 50)
        ])
        inputAccessory.backgroundColor = .gray
        return inputAccessory
    }()
    
}
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Well, UIGestureRecognizer was not necessary to show keyboard when tapped. It just works in the sample code. I want the keyboard appears **before** the view is pushed. Or at least the transition needs to be smooth enough. – shinhong Jan 03 '22 at 18:09
  • if user push a viewcontroller and then return back , this gonna also work . And as you know when viewcontroller in a navigationstack , if user swipe left but not dismiss page viewDidappear is also hired. wouldn't that be a problem? – Omer Tekbiyik Jan 03 '22 at 18:11
  • @shinhong - are you referring to the ***other*** answer I linked to? The tap gesture is there because in that example it also shows/hides/activates the text view. – DonMag Jan 03 '22 at 18:12
  • @shinhong - you need to do a better job of describing your requirements... In your original post, you say *"works just like Messages app in iOS"* ... however, in the iOS Messages app, if you select an **existing** message, the keyboard is ***NOT*** automatically shown. – DonMag Jan 03 '22 at 18:28
  • @DonMag Yes, the first sentence of my comment was about the link you provided. Sorry for the confusion. The solution you provided does work, but in my simulator the delay between view load and keyboard showing up is quite long(~0.5s), and the y axis transition effect of input accessory view is choppy. – shinhong Jan 03 '22 at 18:32
  • @DonMag You're right, I was not careful in describing my requirements. Let me find another example and update the post... – shinhong Jan 03 '22 at 18:34
  • Just updated the post. I'll appreciate if you could look into this once more. – shinhong Jan 03 '22 at 19:00
  • @shinhong - see the **Edit** to my answer. – DonMag Jan 03 '22 at 19:28
  • This workaround seems working. Thank you :) – shinhong Jan 04 '22 at 02:39