You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

UITextView滚动指示器顶部截断问题及官方实现方案咨询

UICollectionViewCell中UITextView滚动指示器截断问题与官方实现参考

示例应用

采用列表布局的UICollectionViewController,包含1个分区和1行。单元格的contentView内包含一个UITextView。

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() // 更推荐在init中配置数据源,可自动避免iOS 15上的不愉快动画,而非在viewDidLoad中配置
    }
    
    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)
        }
    }
}

问题1:修复UITextView垂直滚动指示器顶部截断问题

修改后的核心代码

func configureDataSource() {
    let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in
        // 复用UITextView,避免重复创建实例
        let textViewTag = 1001
        let textView: UITextView
        if let existingTextView = cell.contentView.viewWithTag(textViewTag) as? UITextView {
            textView = existingTextView
        } else {
            textView = UITextView()
            textView.tag = textViewTag
            textView.font = .systemFont(ofSize: UIFont.labelFontSize)
            // 允许滚动指示器超出UITextView边界显示
            textView.clipsToBounds = false
            // 设置滚动指示器内边距,匹配单元格上下内边距
            textView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
            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)
            ])
            // 禁止contentView裁剪子视图,确保滚动指示器完整显示
            cell.contentView.clipsToBounds = false
        }
        textView.text = itemIdentifier
    }
    
    dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
        collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
    }
    
    dataSource?.apply(self.snapshot, animatingDifferences: false)
}

核心修复逻辑

  1. 关闭UITextView.clipsToBounds:默认值为true,会裁剪超出文本视图边界的滚动指示器,关闭后允许指示器部分区域在文本视图外显示
  2. 关闭cell.contentView.clipsToBounds:避免contentView裁剪掉文本视图外的指示器部分
  3. 匹配内边距设置scrollIndicatorInsets:让滚动指示器的位置与文本视图的内边距对齐,保证视觉一致性
  4. 复用UITextView:优化单元格复用性能,避免每次创建新实例

问题2:Table View Cell中UITextView的官方实现参考

苹果官方在UITableViewCell中集成UITextView的标准实现逻辑如下:

  1. 自定义单元格布局:推荐创建UITableViewCell子类,将UITextView添加到cell的contentView中,通过Auto Layout约束控制文本视图与cell内边距的间距
  2. 自适应高度配置:配合UITableView的estimatedRowHeight和rowHeight = UITableView.automaticDimension,实现根据文本内容自动调整单元格高度;若不需要文本滚动,需设置textView.isScrollEnabled = false
  3. 官方文档参考:
    • 《UIKit User Interface Catalog》中UITableViewCell章节明确了可嵌入滚动文本视图的场景
    • 《Auto Layout Guide》详细介绍了动态内容单元格的约束设置方法,确保文本视图与单元格布局协同工作

已尝试但无效的方案

  • textView.verticalScrollIndicatorInsets.top = 30(无效果)
  • 将UITextView添加到自定义视图后再加入单元格contentView
  • 设置以下属性:
    textView.contentInset = .zero
    textView.scrollIndicatorInsets = .zero  
    textView.textContainerInset = .zero
    textView.textContainer.lineFragmentPadding = 0
    
  • 将UITextView垂直居中,并约束其高度与contentView高度相差8pt以保留内边距
  • 将UITextView的上下锚点约束到单元格的layout margins guide的上下锚点

约束条件

出于美观需求,UITextView与单元格顶部和底部需保留一定内边距。

内容的提问来源于stack exchange,提问作者Filippo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 04:43:18