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 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 allow the UI to adjust to text view changes on iOS 16+ only.
Recommended, modern, approach:
Not recommended, old, approach:
I've tried to say view.keyboardLayoutGuide.topAnchor.constraint(equalTo: textView.bottomAnchor).isActive = true
in the text view cell registration, as well as view.keyboardLayoutGuide.topAnchor.constraint(equalTo: collectionView.bottomAnchor).isActive = true
in viewDidLoad()
, but both of these approaches fail (Xcode 15.3 iPhone 15 Pro simulator with iOS 17.4, physical iPhone SE on iOS 15.8).