uicollectionviewuikituitextview

UITextView scrolling indicator cut off at top


Sample app

A collection view controller with list layout, 1 section and 1 row.

The cell's content view contains a text view.

class ViewController: UICollectionViewController {
    var snapshot: NSDiffableDataSourceSnapshot<Section, String> {
        var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
        snapshot.appendSections([.main])
        snapshot.appendItems(["one", "two"], toSection: .main)
        return snapshot
    }
    var dataSource: UICollectionViewDiffableDataSource<Section, String>?
    
    enum Section {
        case main
    }
    
    init() {
        super.init(collectionViewLayout: .init())
        
        collectionView.collectionViewLayout = createLayout()
        configureDataSource() // more likely and automatically avoid unpleasant animations on iOS 15 by configuring the data source in the init rather than in view did load
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configureDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in
            let textView = UITextView()
            textView.font = .systemFont(ofSize: UIFont.labelFontSize)
            
            cell.contentView.addSubview(textView)
            textView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                textView.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 8),
                textView.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -8),
                textView.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: cell.directionalLayoutMargins.leading),
                textView.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -cell.directionalLayoutMargins.trailing)
            ])
        }
        
        dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
            collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
        }
        
        dataSource?.apply(self.snapshot, animatingDifferences: false)
    }
    
    func createLayout() -> UICollectionViewLayout {
        return UICollectionViewCompositionalLayout { section, layoutEnvironment in
            let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
            return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
        }
    }
}

Question 1

Can anybody edit the provided sample code so that the text view's vertical indicator inset is not cut off at the top?

enter image description here

Question 2

It seems to me that Apple has successfully implemented text views inside table view cells: can anybody provide Apple documentation as per how to do so?

What I've tried and didn't work

  1. textView.verticalScrollIndicatorInsets.top = 30 // does nothing
  2. Adding the text view to a custom view and the view to the cell's content view
textView.contentInset = .zero
textView.scrollIndicatorInsets = .zero  
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
  1. Centering the text view vertically and constraining its height to that of the content view with an 8 points constant to leave some padding
  2. Constraining the top and bottom anchors of the text view to the cell's layout margins guide's top and bottom anchors

Constraint

I need the text view to have some padding from the top and bottom of the cell for aesthetic reasons.


Solution

  • The minimum height of the verticalScrollIndicator is 36 ... plus 2-points from the top and 3-points from the bottom.

    So you want the textView height to be at least 41-points:

    NSLayoutConstraint.activate([
        textView.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 8),
        textView.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -8),
        textView.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: cell.directionalLayoutMargins.leading),
        textView.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -cell.directionalLayoutMargins.trailing),
        
        // minimum height to show the full vertical scroll indicator
        textView.heightAnchor.constraint(equalToConstant: 41.0),
    ])
    

    If we use Debug View Hierarchy, we can inspect it (I set the text view background to yellow so we can see the framing):

    enter image description here

    When the app is running, we see this:

    enter image description here

    You don't want to use the font point size to calculate it...

    Suppose the font size is 8.0? Or it's set to 40.0?

    Presumably, you will know what size you're setting your font (after all, you're designing the app).

    So if you're using a small-ish font - like .systemFont(ofSize: UIFont.labelFontSize) - set the height to 41.0 (or greater, as desired).

    If you're using a large font - like .systemFont(ofSize: 40.0) - you'll almost certainly want to set the height greater than 41.0, or you won't see even one line of text.