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

如何实现不同高度单元格的横向集合视图?仿Citymapper效果

嘿,我刚好折腾过类似Citymapper这种横向动态高度列表的需求,给你分享几个实测可行的方案,不管用不用UICollectionView都能搞定!

方案一:基于UICollectionView实现(推荐数据量大时用)

UICollectionView自带复用机制,适合有较多行程项的场景,核心是自定义布局+自动布局适配动态高度:

  • 自定义横向FlowLayout
    先把布局的滚动方向设为横向,再开启自动尺寸估算,或者重写布局方法确保每个cell的高度能动态计算:

    class DynamicHorizontalFlowLayout: UICollectionViewFlowLayout {
        override init() {
            super.init()
            scrollDirection = .horizontal
            // 开启自动尺寸估算,配合cell内的AutoLayout计算高度
            estimatedItemSize = UICollectionViewFlowLayout.automaticSize
            minimumLineSpacing = 16
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        // 可选:如果需要更精准的高度控制,重写该方法调整cell属性
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
            attributes.forEach { attr in
                if attr.representedElementCategory == .cell {
                    // 确保所有cell顶部对齐,高度根据内容自动适配
                    attr.frame.origin.y = 0
                    // 若需要提前缓存高度,这里可以替换为预计算的高度值
                }
            }
            return attributes
        }
    }
    
  • Cell内部的AutoLayout设置
    给自定义Cell的contentView添加完整约束,确保内容能撑高cell:

    class TripCell: UICollectionViewCell {
        private let titleLabel: UILabel = {
            let label = UILabel()
            label.numberOfLines = 0 // 允许多行
            label.translatesAutoresizingMaskIntoConstraints = false
            return label
        }()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            contentView.addSubview(titleLabel)
            // 给label添加上下左右的约束,让contentView根据label内容自动调整高度
            NSLayoutConstraint.activate([
                titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
                titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),
                titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12),
                titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12)
            ])
            contentView.translatesAutoresizingMaskIntoConstraints = false
            // 把contentView的约束连到cell本身,确保cell能跟随contentView的高度变化
            NSLayoutConstraint.activate([
                contentView.topAnchor.constraint(equalTo: topAnchor),
                contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
                contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
                contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
            ])
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        func configure(with trip: Trip) {
            titleLabel.text = trip.title
            // 其他内容配置...
        }
    }
    
  • 预计算高度优化(可选)
    如果担心自动估算的性能问题,可以提前计算每个行程项的高度并缓存,在collectionView(_:layout:sizeForItemAt:)里直接返回缓存的尺寸:

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let trip = trips[indexPath.item]
        let width: CGFloat = 280 // 固定宽度
        let height = calculateTripHeight(trip, width: width) // 自定义计算高度的方法
        return CGSize(width: width, height: height)
    }
    
方案二:非UICollectionView方案(适合数据量小时用)

如果行程项不多,直接用UIScrollView动态添加子视图会更灵活,不用处理collectionView的复用逻辑:

  • 搭建ScrollView并动态添加行程视图
    初始化ScrollView后,遍历数据源创建自定义的行程视图,用AutoLayout横向排列,每个视图的高度由自身内容决定:

    func setupTripScrollView() {
        let scrollView = UIScrollView()
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.showsHorizontalScrollIndicator = false
        view.addSubview(scrollView)
        
        // 先给scrollView设置基础约束(宽度占满屏幕,高度可以先设一个默认值,之后再调整)
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.heightAnchor.constraint(equalToConstant: 150)
        ])
        
        var previousTripView: UIView?
        for trip in trips {
            let tripView = TripCustomView(trip: trip)
            tripView.translatesAutoresizingMaskIntoConstraints = false
            scrollView.addSubview(tripView)
            
            // 固定宽度,高度由内部内容自动计算
            NSLayoutConstraint.activate([
                tripView.widthAnchor.constraint(equalToConstant: 280),
                tripView.topAnchor.constraint(equalTo: scrollView.topAnchor),
                tripView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
            ])
            
            // 横向排列,和前一个视图保持间距
            if let prevView = previousTripView {
                tripView.leadingAnchor.constraint(equalTo: prevView.trailingAnchor, constant: 16).isActive = true
            } else {
                tripView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16).isActive = true
            }
            
            previousTripView = tripView
        }
        
        // 给最后一个视图添加尾部约束,让scrollView的contentSize自动适配
        if let lastView = previousTripView {
            lastView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -16).isActive = true
        }
        
        // 可选:动态调整scrollView的高度为所有子视图中的最大高度
        DispatchQueue.main.async {
            let maxHeight = scrollView.subviews.map { $0.frame.height }.max() ?? 150
            scrollView.heightAnchor.constraint(equalToConstant: maxHeight).isActive = true
        }
    }
    
  • 自定义TripCustomView
    和Cell的逻辑一样,内部用AutoLayout约束好所有内容,确保视图能根据内容自动调整高度,比如label设为多行,图片约束好尺寸等。

关键注意事项
  • 内容自适应核心:不管用哪种方案,都要确保每个“单元格”的内容通过完整的AutoLayout约束来撑高,避免出现约束不完整导致高度计算错误的情况。
  • 对齐方式:Citymapper的行程项是顶部对齐的,所以要保证所有子视图/Cell的顶部在同一水平线上,高度各自适配内容。
  • 性能考量:数据量多(比如超过20项)时优先用UICollectionView,复用机制能减少内存占用;数据量少的话用UIScrollView方案更省心。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 07:32:15