Handle keyboard layout in iOS 15+ UIKit app with collection view using the modern approach

The following is a UIKit app that uses a collection view with list layout and a diffable data source.

It displays one section that has 10 empty cells and then a final cell whose content view contains a text view, that is pinned to the content view's layout margins guide.

The text view's scrolling is set to false, so that the line collectionView.selfSizingInvalidation = .enabledIncludingConstraints will succeed at making the text view's cell resize automatically and animatedly as the text changes.

import UIKit

class ViewController: UIViewController {
    var collectionView: UICollectionView!
    
    var dataSource: UICollectionViewDiffableDataSource<String, Int>!
    
    let textView: UITextView = {
      let tv = UITextView()
        tv.text = "Text"
        tv.isScrollEnabled = false
        return tv
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        configureHierarchy()
        configureDataSource()
        
        if #available(iOS 16.0, *) {
            collectionView.selfSizingInvalidation = .enabledIncludingConstraints
        }
    }

    func configureHierarchy() {
        collectionView = .init(frame: .zero, collectionViewLayout: createLayout())
        view.addSubview(collectionView)
        collectionView.frame = view.bounds
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    }
    
    func createLayout() -> UICollectionViewLayout {
        let configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        return UICollectionViewCompositionalLayout.list(using: configuration)
    }
    
    func configureDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { _, _, _ in }
        let textViewCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { [weak self] cell, _, _ in
            guard let self else { return }
            
            cell.contentView.addSubview(textView)
            textView.pin(to: cell.contentView.layoutMarginsGuide)
        }
        
        dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
            if indexPath.row == 10 {
                collectionView.dequeueConfiguredReusableCell(using: textViewCellRegistration, for: indexPath, item: itemIdentifier)
            } else {
                collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
            }
        }
        
        var snapshot = NSDiffableDataSourceSnapshot<String, Int>()
        snapshot.appendSections(["section"])
        snapshot.appendItems(Array(0...10))
        dataSource.apply(snapshot)
    }
}

extension UIView {
    func pin(
        to object: CanBePinnedTo,
        top: CGFloat = 0,
        bottom: CGFloat = 0,
        leading: CGFloat = 0,
        trailing: CGFloat = 0
    ) {
        self.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            self.topAnchor.constraint(equalTo: object.topAnchor, constant: top),
            self.bottomAnchor.constraint(equalTo: object.bottomAnchor, constant: bottom),
            self.leadingAnchor.constraint(equalTo: object.leadingAnchor, constant: leading),
            self.trailingAnchor.constraint(equalTo: object.trailingAnchor, constant: trailing),
        ])
    }
}

@MainActor
protocol CanBePinnedTo {
    var topAnchor: NSLayoutYAxisAnchor { get }
    var bottomAnchor: NSLayoutYAxisAnchor { get }
    var leadingAnchor: NSLayoutXAxisAnchor { get }
    var trailingAnchor: NSLayoutXAxisAnchor { get }
}

extension UIView: CanBePinnedTo { }
extension UILayoutGuide: CanBePinnedTo { }

How do I make the UI move to accomodate the keyboard once you tap on the text view and also when the text view changes size, by activating the view.keyboardLayoutGuide.topAnchor constraint, as shown in the WWDC21 video "Your guide to keyboard layout"?

My code does not resize the text view on iOS 15, only on iOS 16+, so clearly the solution may as well allow the UI to adjust to changes to the text view frame on iOS 16+ only.

Recommended, modern, approach:

Not recommended, old, approach:

Here's what I've tried and didn’t work on the Xcode 15.3 iPhone 15 Pro simulator with iOS 17.4 and on my iPhone SE with iOS 15.8:

  1. view.keyboardLayoutGuide.topAnchor.constraint(equalTo: textView.bottomAnchor).isActive = true in the text view cell registration
  2. view.keyboardLayoutGuide.topAnchor.constraint(equalTo: collectionView.bottomAnchor).isActive = true in viewDidLoad()
  3. pinning the bottom of the collection view to the top of the keyboard:
func configureHierarchy() {
    collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout())
    view.addSubview(collectionView)
    collectionView.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
       collectionView.topAnchor.constraint(equalTo: view.topAnchor),
       collectionView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor),
       collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
       collectionView.trailingAnchor.constraint(equalTo:  view.trailingAnchor)
    ])
}

To be more specific, I only tried this last approach on the simulator, and it moved the UI seemingly twice as much as it should have, and the tab bar of my tab bar controller was black, which discouraged me although there might be a proper (non-workaround) solution for iOS 17 only (i.e. saying view.keyboardLayoutGuide.usesBottomSafeArea = false).

The first 2 approaches just didn't work instead.

Setting the constraints priority to .defaultHigh doesn't do it.

Answered by DTS Engineer in 797384022

I highly recommend that you use UICollectionViewController, which automatically handles the keyboard avoidance for you. You can give it a try, if haven’t yet, and see if that fits your UI.

If you really need to use UIViewController + UICollectionView, as shown in your sample project, we indeed don’t have a documented way to use UIKeyboardLayoutGuide, which I think is worth a feedback report.

By playing with your sample project, I see that the following configuration works for me:

    func configureHierarchy() {
        ...
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
    }

There are, however, some corner cases where the above configuration may not just work. If that happens in your real-world app, I suggest that you file a feedback report against that and share your report ID here.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

@Filippo02 There's isn't a specifc API for this. You'll need to move the collectionView frame y origin.

For example:



func keyboardWillShow(notification: NSNotification) {
    guard let userInfo = notification.userInfo,
          let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
        return
    }

    let keyboardSize = keyboardFrame.cgRectValue
    if view.frame.origin.y == 0 {
        UIView.animate(withDuration: 0.3) { [weak self] in
            self?.view.frame.origin.y -= keyboardSize.height
        }
    }
}

func keyboardWillHide(notification: NSNotification) {
    if view.frame.origin.y != 0 {
        UIView.animate(withDuration: 0.3) { [weak self] in
            self?.view.frame.origin.y = 0
        }
    }
}

I can't delete this reply

@DTS Engineer I see, but it looks like this approach is no longer recommended: I can't find "The Keys to a Better Text Input Experience", the WWDC session that is mentioned in "Your guide to keyboard layout". Considering that it is impossible that Apple does not explain you how to comprehensively (as explained later) handle the keyboard being laid out if your app uses a collection view controller and that I can't find such documentation, do you happen to have a link to it?

In particular, your solution does not take into account the fact that the keyboard frame might change, the fact that the text view might have input accessory views, that the view controller might be embedded in a collection view controller, that the app scene might be floating on an iPad or that there have been changes in the timing with which the keyboard is shown in iOS 17 with respect to earlier iOS versions. All of these cases and more are handled by the up to date API.

Accepted Answer

I highly recommend that you use UICollectionViewController, which automatically handles the keyboard avoidance for you. You can give it a try, if haven’t yet, and see if that fits your UI.

If you really need to use UIViewController + UICollectionView, as shown in your sample project, we indeed don’t have a documented way to use UIKeyboardLayoutGuide, which I think is worth a feedback report.

By playing with your sample project, I see that the following configuration works for me:

    func configureHierarchy() {
        ...
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
    }

There are, however, some corner cases where the above configuration may not just work. If that happens in your real-world app, I suggest that you file a feedback report against that and share your report ID here.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Handle keyboard layout in iOS 15+ UIKit app with collection view using the modern approach
 
 
Q