如何实现不同高度单元格的横向集合视图?仿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
相关产品推荐
相关产品推荐

