Keyboard Handling in iOS using Swift 5

Written by nQaze | Published 2020/05/14
Tech Story Tags: swift | ios-app-development | ios-development | keyboard | code | mobile-app-development | coding | application

TLDR Keyboard Handling in iOS using Swift 5.0 is a topic or rather a problem since the inception of iOS Development. When you tap on a text-field, the keyboard pops up and allows you to type in whatever you want. There is no default done button which will dismiss the keyboard. Tapping outside the keyboard area also does not make that thing go away! Apple has not yet made the dismissal of the keyboard a default thing. Fortunately, there are several ways in which we can achieve this functionality.via the TL;DR App

“Handling Keyboard in iOS” — This has been a topic or rather a problem since the inception of iOS Development. Whether you are a beginner developer or an experienced one, you’ll always be dealing with the handling of keyboards in your apps! But there’s not just 1 issue with the keyboard that we are talking about here. We have to manually handle a couple of issues. Here, let me show you exactly what I mean -
Manually handle the dismissal of the keyboard
When you tap on a text-field, the keyboard pops up and allows you to type in whatever you want. Now once you are done writing, there is no default done button which will dismiss the keyboard, tapping outside the keyboard ain’t gonna do anything either. It’s like iOS saying — “Isn’t it enough I popped up the keyboard automatically for you? Figure out a way to make it go away on your own now!” Sure thing boss! If that’s the protocol, we will follow.
Moving text-fields on your screen when the Keyboard appears & disappears
When your text-field is placed in the lower area of your screen like for example a chat application, you’ll come across this scenario — You tap on it and the keyboard appears and your text-field should move upwards along with the keyboard. This does not happen by default on iOS! What happens is — when you tap on the text-field, the keyboard sure appears, but now your text-field is no longer visible!
The keyboard just overlaps the text-field on the screen that was placed near the bottom of the screen. Yes, you heard it right! iOS won’t automatically shift the text-field as per the keyboard.
When I was a beginner in iOS development, this issue caught me by surprise! Mainly because I code in Android too and this behavior is handled very gracefully by default over there. Never knew something as simple as a keyboard would have to be handled manually.
Enough of complaints. Where there is a problem, there ought to be a solution. Our jobs as engineers and developers are to find answers to problems like this and solve them elegantly.
There are a lot of ways in which people have already solved this problem. For now, I’ll be walking you through the findings and solutions that I use in my day to day projects.

Problem 1 — Handling Keyboard dismissal (How do I make this Keyboard go away?)

Like we were discussing above, keyboards in iOS don’t disappear on its own. The return key on the keyboard does nothing by default! Also, there is no return key for Numeric Keypad! Tapping outside the keyboard area also does not make that thing go away! Whhattt???
Unfortunately, Apple has not yet made the dismissal of the keyboard a default thing. Hence we developers need to handle this scenario programmatically in our app. Fortunately, there are several ways in which we can achieve this functionality and that too within a few lines of code.
Which way to implement in your code may depend on your exact scenario. You can judge the best one and use the same as per your requirement.

Via Tap Gesture

This is the quickest way to implement keyboard dismissal. Just set a Tap gesture on the main View and hook that gesture with a function which calls
view.endEditing
Apple docs say the following about
endEditing
- Causes the view (or one of its embedded text fields) to resign the first responder status.

That’s it! Now when you tap on a text-field and keyboard appears, just tap outside anywhere on the view and your keyboard would be dismissed
The below code can be shortened into just 2 lines but I have specifically broken into functions to help make this clear as possible. I have also added comments  in the code to help understand what each function does. 

class ChatLoginVC: UIViewController {
 
     override func viewDidLoad() {
        super.viewDidLoad()
        //We make a call to our keyboard handling function as soon as the view is loaded.
        initializeHideKeyboard()
    }
 
 }
 
 extension ChatVC {
 
     func initializeHideKeyboard(){
        //Declare a Tap Gesture Recognizer which will trigger our dismissMyKeyboard() function
        let tap: UITapGestureRecognizer = UITapGestureRecognizer(
            target: self,
            action: #selector(dismissMyKeyboard))
        
        //Add this tap gesture recognizer to the parent view
        view.addGestureRecognizer(tap)
    }
    
    @objc func dismissMyKeyboard(){
        //endEditing causes the view (or one of its embedded text fields) to resign the first responder status.
        //In short- Dismiss the active keyboard.
        view.endEditing(true)
    }
 
 }

Via Keyboard Return Key

Another great option is to use that non-functional keyboard Return/Done/Continue key. It’s just sitting there doing nothing unless we have specified some custom behavior in
textFieldShouldReturn 
function. 
textFieldShouldReturn
asks the delegate if the text field should process the pressing of the return button.
  • Here you would first have to set the delegates for the text-fields. 
  • Set Tag to the text-fields. What is a Tag, you ask? Apple documentation says - An integer that you can use to identify view objects in your application. The setting of Tag is optional and would not be required when you have only 1 text-field. But here I am assigning Tag to the text-fields by incrementing their value by 1 and in the order, they are placed on the screen. This would help us identify the text-fields in code.
  • textFieldShouldReturn
    fires when the user presses the Return key on the keyboard. Hence, here we check - Is there any other text-field in the view whose tag is +1 greater than the current text-field on which the return key was pressed. If yes → then move the cursor to that next text-field. If No → Dismiss the keyboard.
Also don’t worry about that password field not visible when typing. We’ll solve that issue when we discuss about moving text-fields to visibility as per the keyboard in the later section of this article.

class ChatLoginVC: UIViewController {
 
     @IBOutlet weak var emailTF: UITextField!
     @IBOutlet weak var passwordTF: UITextField!
     
     override func viewDidLoad() {
         super.viewDidLoad()
         emailTF.delegate = self
         passwordTF.delegate = self
         
         emailTF.tag = 1
         passwordTF.tag = 2
     }
 
 }
 
 extension ChatLoginVC: UITextFieldDelegate {
 
     func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        //Check if there is any other text-field in the view whose tag is +1 greater than the current text-field on which the return key was pressed. If yes → then move the cursor to that next text-field. If No → Dismiss the keyboard
        if let nextField = self.view.viewWithTag(textField.tag + 1) as? UITextField {
            nextField.becomeFirstResponder()
        } else {
            textField.resignFirstResponder()
        }
        return false
    }
 }

Via Toolbar for Number Pad

The above solution with
textFieldShouldReturn
works great but unfortunately, there is no “Return” key on iOS Number Pad. To overcome our barrier here in the case of Number Pad, we can add a simple Toolbar above our Keyboard with a “Done” button.
This “Done” button is going to call the same function we used in our Tap Gesture Method above and would dismiss our keyboard. To keep consistency across the fields, you can use Toolbars across the app as a common patter to dismiss your keyboard.

class ChatLoginVC: UIViewController {
 
     @IBOutlet weak var pinTF: UITextField!
     
     override func viewDidLoad() {
         super.viewDidLoad()
         setupToolbar()
     }
     
     func setupToolbar(){
        //Create a toolbar
        let bar = UIToolbar()
        
        //Create a done button with an action to trigger our function to dismiss the keyboard
        let doneBtn = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(dismissMyKeyboard))
        
        //Create a felxible space item so that we can add it around in toolbar to position our done button
        let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
        
        //Add the created button items in the toobar
        bar.items = [flexSpace, flexSpace, doneBtn]
        bar.sizeToFit()
        
        //Add the toolbar to our textfield
        pinTF.inputAccessoryView = bar
    }
     
     @objc func dismissMyKeyboard(){
         view.endEditing(true)
     }
 
 }
I found a nice little extension for creating Done Button Toolbar which will help you reduce the code duplication and hook the same function across the app - SWIFT - Add keyboard Done button using UIToolbar
If you need more insights & ways on dealing with Keyboard dismissal, I found this great article on medium, You can find it here - Best way to dismiss Keyboard in a View Controller iOS (Swift)

Problem 2 — Moving text-fields as per the keyboard (Where is my text-field?)

We need to see our text while typing, that’s basic UX. Unfortunately, when your text-field is placed at the bottom area of your screen like for example in a chat application, your keyboard is going to overlap it as soon as you tap on the field. We need to handle this programmatically beforehand.
What is our expected behavior here?
First, we need to check whenever a field is tapped and the keyboard appears, whether the keyboard is going to overlap the field (which in our chat application's case, it will)? If so → scroll the view such that your text-field appears just above the keyboard.
If your text-field is not overlapped by the keyboard, then there is no need to shift/scroll the screen. Enough about the problem, let’s take a deep dive into the solution.  I have added  comments in the below code to help in better understanding the role of each function in achieving  our desired goal. 

class ChatVC: UIViewController {

    @IBOutlet weak var messageTF: UITextField!
    @IBOutlet weak var backgroundSV: UIScrollView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //Subscribe to a Notification which will fire before the keyboard will show
        subscribeToNotification(UIResponder.keyboardWillShowNotification, selector: #selector(keyboardWillShowOrHide))
        
        //Subscribe to a Notification which will fire before the keyboard will hide
        subscribeToNotification(UIResponder.keyboardWillHideNotification, selector: #selector(keyboardWillShowOrHide))
        
        //We make a call to our keyboard handling function as soon as the view is loaded.
        initializeHideKeyboard()
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        //Unsubscribe from all our notifications
        unsubscribeFromAllNotifications()
    }
}

extension ChatVC {
    
    func initializeHideKeyboard(){
        //Declare a Tap Gesture Recognizer which will trigger our dismissMyKeyboard() function
        let tap: UITapGestureRecognizer = UITapGestureRecognizer(
            target: self,
            action: #selector(dismissMyKeyboard))
        
        //Add this tap gesture recognizer to the parent view
        view.addGestureRecognizer(tap)
    }
    
    @objc func dismissMyKeyboard(){
        //endEditing causes the view (or one of its embedded text fields) to resign the first responder status.
        //In short- Dismiss the active keyboard.
        view.endEditing(true)
    }
}

extension ChatVC {
    
    func subscribeToNotification(_ notification: NSNotification.Name, selector: Selector) {
        NotificationCenter.default.addObserver(self, selector: selector, name: notification, object: nil)
    }
    
    func unsubscribeFromAllNotifications() {
        NotificationCenter.default.removeObserver(self)
    }
    
    @objc func keyboardWillShowOrHide(notification: NSNotification) {
        // Get required info out of the notification
        if let scrollView = backgroundSV, let userInfo = notification.userInfo, let endValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey], let durationValue = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey], let curveValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] {
            
            // Transform the keyboard's frame into our view's coordinate system
            let endRect = view.convert((endValue as AnyObject).cgRectValue, from: view.window)
            
            // Find out how much the keyboard overlaps our scroll view
            let keyboardOverlap = scrollView.frame.maxY - endRect.origin.y
            
            // Set the scroll view's content inset & scroll indicator to avoid the keyboard
            scrollView.contentInset.bottom = keyboardOverlap
            scrollView.scrollIndicatorInsets.bottom = keyboardOverlap
            
            let duration = (durationValue as AnyObject).doubleValue
            let options = UIView.AnimationOptions(rawValue: UInt((curveValue as AnyObject).integerValue << 16))
            UIView.animate(withDuration: duration!, delay: 0, options: options, animations: {
                self.view.layoutIfNeeded()
            }, completion: nil)
        }
    }
}

Problem 3 — Repetition of code

We would most probably have multiple text-field in our Applications. And these text-fields would be scattered across our entire application on different Screens. The above solution we implemented was in single ViewController. So how do we go about implementing these solutions in all the ViewControllers wherever we need to handle our Keyboard?
The first thing that would come to our minds is to add the above code snippets in all the ViewControllers wherever we need to handle such scenarios. But that means we would have to keep manually adding this code in every ViewController.
There has to be a better way!
Thankfully, there is! We would be using some basic concept of OOP over here - Inheritance. We would be declaring a base keyboard handling class which would be inherited from our default UIViewController. Now wherever we need to handle our keyboard, we would just inherit our class from our base class.
  • Declare a base class for handling keyboard
  • Write our entire keyboard handling code in that base class
  • Inherit your ViewControllers from this base class and Voila! You have handled your keyboard issues elegantly.
We'll see how this looks in code -

class ChatVC: KeyboardHandlingBaseVC {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

class KeyboardHandlingBaseVC: UIViewController {

    @IBOutlet weak var backgroundSV: UIScrollView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        subscribeToNotification(UIResponder.keyboardWillShowNotification, selector: #selector(keyboardWillShowOrHide))
        subscribeToNotification(UIResponder.keyboardWillHideNotification, selector: #selector(keyboardWillShowOrHide))
        
        initializeHideKeyboard()
        
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        unsubscribeFromAllNotifications()
    }

}

// MARK: Keyboard Dismissal Handling on Tap

private extension KeyboardHandlingBaseVC {
    
    func initializeHideKeyboard(){
        let tap: UITapGestureRecognizer = UITapGestureRecognizer(
            target: self,
            action: #selector(dismissMyKeyboard))
        
        view.addGestureRecognizer(tap)
    }
    
    @objc func dismissMyKeyboard(){
        view.endEditing(true)
    }
}

// MARK: Textfield Visibility Handling with Scroll

private extension KeyboardHandlingBaseVC {
    
    func subscribeToNotification(_ notification: NSNotification.Name, selector: Selector) {
        NotificationCenter.default.addObserver(self, selector: selector, name: notification, object: nil)
    }
    
    func unsubscribeFromAllNotifications() {
        NotificationCenter.default.removeObserver(self)
    }
    
    @objc func keyboardWillShowOrHide(notification: NSNotification) {
        if let scrollView = backgroundSV, let userInfo = notification.userInfo, let endValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey], let durationValue = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey], let curveValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] {
            
            let endRect = view.convert((endValue as AnyObject).cgRectValue, from: view.window)
            let keyboardOverlap = scrollView.frame.maxY - endRect.origin.y
            scrollView.contentInset.bottom = keyboardOverlap
            scrollView.scrollIndicatorInsets.bottom = keyboardOverlap
            
            let duration = (durationValue as AnyObject).doubleValue
            let options = UIView.AnimationOptions(rawValue: UInt((curveValue as AnyObject).integerValue << 16))
            UIView.animate(withDuration: duration!, delay: 0, options: options, animations: {
                self.view.layoutIfNeeded()
            }, completion: nil)
        }
    }

}

Bonus — Let’s see some Libraries

So let’s say you don’t want to handle any of the issues mentioned above manually. There may be various reasons for that but yeah the possibility exists. It’s fine, sometimes time is of great essence. Don’t worry, we have got that aspect also covered in this article. There are various libraries out there that solve the Keyboard Dismissal and TextField handling problem. I’ll list down a couple so that you can go through their read-me and find for yourself whichever suits your needs.

Conclusion

So far we explored the problem of keyboard dismissal & TextField handling with keyboard. We also explored some of the ways in which we can easily solve these problems along with avoiding code repetition in our project. There is no such good or bad way when it comes to solving a technical problem.
What matters the most is - At that particular moment, what works for you?
what works for the project?  
what works for your team?
There may be times when you would need to use a mix of the above techniques or sometimes one single pattern can be followed across the app! At the end of the day what matters is that you solved the problem  
That’s all from this article, do share if you all have any other ideas & approaches to solve this particular problem. I would be happy to discuss the same!
This article was originally published at CometChat.
If you enjoyed reading this article and learned something, share it amongst your developer friends/colleagues and spread the knowledge! If you have anything to discuss regarding Keyboard Handling or any other topic related to Mobile Apps, Development, Designing, Engineering… Drop a comment below or DM me on Twitter :)
If you find this interesting, chances are you’ll find my twitter feed too! I keep sharing articles and stuff related to development, design and productivity on a daily basis. So hop on, follow me on Twitter and enjoy the learning ride!
Till the next Article…

Written by nQaze | Software Engineer | Product Designer
Published by HackerNoon on 2020/05/14