uicollectionviewuikituitextview

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: recommended, modern, approach

Not recommended, old, 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.


Solution

  • Credit: https://developer.apple.com/forums/thread/756691?answerId=797384022#797384022.

    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.