iosuitableviewuinavigationcontrolleruitextviewimessage

iOS UITableView content goes under UINavigationBar weird bug when inputAccessoryView containing UITextView shows keyboard


I have the following example which shows the issue.

I basically want to show a UI similar to iMessage where a UITextView is present at the bottom for typing the message.

Once the keyboard is shown in the UITextView, upon scrolling the UITableView even slightly, the content goes under the navigation bar while the navigation bar stays transparent. It seems like a weird UI bug.

import UIKit
import SnapKit

let numberOfRows = 15

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    @IBOutlet var myTableView: UITableView!
    
    let textView = UITextView()
    let bottomView = UIView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        myTableView.keyboardDismissMode = .interactive
        
        bottomView.backgroundColor = .darkGray
        bottomView.addSubview(textView)
     
        view.addSubview(bottomView)
        bottomView.snp.makeConstraints { make in
            make.left.right.bottom.equalToSuperview()
            make.height.equalTo(100)
        }
        
        textView.backgroundColor = .black
        textView.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(10)
        }
        textView.text = "Type something.."
        
    }
    
    override var inputAccessoryView: UIView {
        return bottomView
    }
        
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return UITableView.automaticDimension
    }
    
    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        return 200
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return numberOfRows
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
        
        cell.textLabel?.text = "Row \(indexPath.row)"
        cell.detailTextLabel?.text = "Tastes very close to Snickers chocolate bar. Only tried because of reviews. Would purchase again if price wasn’t so high, and available in Canada."
        
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        navigationController?.pushViewController(UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "DetailVC"), animated: true)
    }

}

Screenshot of the bug after scrolling even a tiny bit after keyboard is shown:

enter image description here


Solution

  • I experience the same behavior as you with this code. There is definitely something confusing the top safe area guide.

    I solved the problem by abandoning this inputAccessoryView pattern altogether. I just

    enter image description here

    Having done that, you enjoy the “keep the text view right above the keyboard” functionality, without this idiosyncratic navigation bar behavior/bug.

    This approach also enjoys the automatic adjustment of the tableview scroll area so that you can still scroll to the bottom even if the keyboard is presented. It also is a zero-code solution.


    FWIW, if doing this in IB, you may have to select the main view and explicitly enable the keyboard layout guides: enter image description here


    In iOS versions prior to iOS 15, we used to have to add keyboard observers. So, we would add an @IBOutlet for the bottom constraint to the superview and then manually change that bottom constraint (animating it alongside the presentation and dismissal of the keyboard). E.g., I have done something along those lines in my pre iOS 15 projects:

    @IBOutlet weak var bottomConstraint: NSLayoutConstraint!
    private var keyboardWillShowObserver: NSObjectProtocol?
    private var keyboardWillHideObserver: NSObjectProtocol?
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        addKeyboardObservers()
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        removeKeyboardObservers()
    }
    

    With:

    private extension ViewController {
        func addKeyboardObservers() {
            keyboardWillShowObserver = NotificationCenter.default.addObserver(
                forName: UIApplication.keyboardWillShowNotification,
                object: nil,
                queue: .main
            ) { [weak self] notification in
                guard let self else { return }
                showKeyboard(bottom: bottomConstraint, notification: notification)
            }
    
            keyboardWillHideObserver = NotificationCenter.default.addObserver(
                forName: UIApplication.keyboardWillHideNotification,
                object: nil,
                queue: .main
            ) { [weak self] notification in
                guard let self else { return }
                hideKeyboard(bottom: bottomConstraint, notification: notification)
            }
        }
    
        func removeKeyboardObservers() {
            if let keyboardWillShowObserver {
                NotificationCenter.default.removeObserver(keyboardWillShowObserver)
            }
    
            if let keyboardWillHideObserver {
                NotificationCenter.default.removeObserver(keyboardWillHideObserver)
            }
        }
    }
    

    And that uses this UIViewController extension:

    //  UIViewController+KeyboardChange.swift
    
    import UIKit
    
    extension UIViewController {
        func showKeyboard(bottom constraint: NSLayoutConstraint, notification: Notification) {
            animateAlongsideKeyboardChange(notification) { [self] frame in
                constraint.constant = view.bounds.height - frame.minY
                view.layoutIfNeeded()
            }
        }
    
        func hideKeyboard(bottom constraint: NSLayoutConstraint, notification: Notification) {
            animateAlongsideKeyboardChange(notification) { [self] _ in
                constraint.constant = 0
                view.layoutIfNeeded()
            }
        }
    
        private func animateAlongsideKeyboardChange(_ notification: Notification, animation: @escaping (CGRect) -> Void) {
            let userInfo = notification.userInfo!
            guard
                var keyboardFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue,
                let curve = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber).flatMap({
                    return UIView.AnimationCurve(rawValue: $0.intValue)
                }),
                let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber as? CGFloat
            else {
                return
            }
    
            keyboardFrame = view.convert(keyboardFrame, from: nil)
    
            UIViewPropertyAnimator(duration: duration, curve: curve) {
                animation(keyboardFrame)
            }.startAnimation()
        }
    }
    

    This is all a bit cumbersome, which is why the iOS 15 keyboard layout guides were so welcomed. But in earlier iOS versions, we would observe the presentation and dismissal of the keyboard manually.