iOS中UITableViewCell内动态状态节点视图实现方案问询
动态物流节点视图实现方案(集成到UITableViewCell)
嘿,我来帮你搞定这个动态节点视图的需求!之前用Storyboard做固定数量的版本没问题,但要适配API返回的动态节点数,这里有两个实用的方案,你可以根据需求选:
方案一:纯代码+Auto Layout动态创建视图(适合节点数较少的场景)
这个方案直接在自定义UITableViewCell里动态生成每个节点的圆和连接线,用Auto Layout确保布局自适应,步骤如下:
- 自定义UITableViewCell子类,先搭好基础的容器视图,用来统一管理所有节点元素
- 编写配置方法,接收API返回的状态数组,动态创建子视图
- 处理布局逻辑:第一个节点左对齐容器,后续节点和前一个保持固定间距,同时在中间添加连接线;最后一个节点不需要连接线
- 根据状态样式化:完成的节点用绿色,当前节点用蓝色,未完成的用灰色(你可以根据需求调整)
代码示例
import UIKit // 先定义状态模型,对应API返回的结构 struct TrackingStatus { let title: String // 节点文字,比如"已下单" let isCompleted: Bool // 是否已完成 let isCurrent: Bool // 是否是当前节点 } class TrackingStatusCell: UITableViewCell { // 容器视图,用来装所有节点和线 private let containerView = UIView() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupBaseUI() } required init?(coder: NSCoder) { super.init(coder: coder) setupBaseUI() } // 初始化基础布局 private func setupBaseUI() { contentView.addSubview(containerView) containerView.translatesAutoresizingMaskIntoConstraints = false // 给容器视图加约束,和cell保持边距 NSLayoutConstraint.activate([ containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16) ]) } // 核心配置方法,传入状态数组 func configure(with statuses: [TrackingStatus]) { // 先清空之前的子视图,避免cell复用重复添加 containerView.subviews.forEach { $0.removeFromSuperview() } var previousCircle: UIView? for (index, status) in statuses.enumerated() { // 1. 创建节点圆 let circle = UIView() circle.translatesAutoresizingMaskIntoConstraints = false circle.layer.cornerRadius = 10 // 圆的半径,按需调整 // 根据状态设置颜色 circle.backgroundColor = status.isCompleted ? .systemGreen : status.isCurrent ? .systemBlue : .lightGray containerView.addSubview(circle) // 布局圆的垂直居中,固定大小 NSLayoutConstraint.activate([ circle.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), circle.widthAnchor.constraint(equalToConstant: 20), circle.heightAnchor.constraint(equalToConstant: 20) ]) // 2. 处理水平布局和连接线 if index == 0 { // 第一个圆左对齐容器 circle.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true } else { guard let prevCircle = previousCircle else { continue } // 后续圆和前一个圆保持固定间距 circle.leadingAnchor.constraint(equalTo: prevCircle.trailingAnchor, constant: 40).isActive = true // 添加连接线 let line = UIView() line.translatesAutoresizingMaskIntoConstraints = false line.backgroundColor = .lightGray containerView.addSubview(line) NSLayoutConstraint.activate([ line.leadingAnchor.constraint(equalTo: prevCircle.trailingAnchor), line.trailingAnchor.constraint(equalTo: circle.leadingAnchor), line.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), line.heightAnchor.constraint(equalToConstant: 1) ]) } // 3. 添加状态文字(可选,按需添加) let statusLabel = UILabel() statusLabel.translatesAutoresizingMaskIntoConstraints = false statusLabel.text = status.title statusLabel.font = UIFont.systemFont(ofSize: 12) statusLabel.textColor = status.isCompleted ? .label : .secondaryLabel statusLabel.textAlignment = .center containerView.addSubview(statusLabel) NSLayoutConstraint.activate([ statusLabel.topAnchor.constraint(equalTo: circle.bottomAnchor, constant: 8), statusLabel.centerXAnchor.constraint(equalTo: circle.centerXAnchor) ]) previousCircle = circle } } }
方案二:内嵌UICollectionView(适合节点数多、需要横向滚动的场景)
如果节点数量可能超过屏幕宽度,或者需要横向滚动查看,这个方案更合适:把每个节点做成CollectionView的Item,然后把CollectionView内嵌到UITableViewCell里。
代码示例
import UIKit struct TrackingStatus { let title: String let isCompleted: Bool let isCurrent: Bool } // 自定义CollectionViewCell,单个节点的视图 class TrackingNodeCell: UICollectionViewCell { private let circleView = UIView() private let statusLabel = UILabel() private let lineView = UIView() // 控制是否显示右侧的连接线 var showLine: Bool = true { didSet { lineView.isHidden = !showLine } } override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { super.init(coder: coder) setupUI() } private func setupUI() { // 布局节点圆 circleView.translatesAutoresizingMaskIntoConstraints = false circleView.layer.cornerRadius = 10 contentView.addSubview(circleView) // 布局状态文字 statusLabel.translatesAutoresizingMaskIntoConstraints = false statusLabel.font = UIFont.systemFont(ofSize: 12) statusLabel.textAlignment = .center statusLabel.numberOfLines = 0 contentView.addSubview(statusLabel) // 布局连接线 lineView.translatesAutoresizingMaskIntoConstraints = false lineView.backgroundColor = .lightGray contentView.addSubview(lineView) NSLayoutConstraint.activate([ circleView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), circleView.topAnchor.constraint(equalTo: contentView.topAnchor), circleView.widthAnchor.constraint(equalToConstant: 20), circleView.heightAnchor.constraint(equalToConstant: 20), statusLabel.topAnchor.constraint(equalTo: circleView.bottomAnchor, constant: 8), statusLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), statusLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), statusLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), lineView.leadingAnchor.constraint(equalTo: circleView.trailingAnchor), lineView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), lineView.centerYAnchor.constraint(equalTo: circleView.centerYAnchor), lineView.heightAnchor.constraint(equalToConstant: 1) ]) } func configure(with status: TrackingStatus) { circleView.backgroundColor = status.isCompleted ? .systemGreen : status.isCurrent ? .systemBlue : .lightGray statusLabel.text = status.title statusLabel.textColor = status.isCompleted ? .label : .secondaryLabel } } // 内嵌CollectionView的UITableViewCell class TrackingStatusCell: UITableViewCell, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { private let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) private var statuses: [TrackingStatus] = [] override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupCollectionView() } required init?(coder: NSCoder) { super.init(coder: coder) setupCollectionView() } private func setupCollectionView() { // 设置横向滚动的FlowLayout let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.minimumInteritemSpacing = 20 // 节点之间的间距 collectionView.collectionViewLayout = layout collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.backgroundColor = .clear collectionView.showsHorizontalScrollIndicator = false collectionView.register(TrackingNodeCell.self, forCellWithReuseIdentifier: "TrackingNodeCell") collectionView.dataSource = self collectionView.delegate = self contentView.addSubview(collectionView) // 给CollectionView加约束 NSLayoutConstraint.activate([ collectionView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16), collectionView.heightAnchor.constraint(greaterThanOrEqualToConstant: 60) // 确保足够显示内容 ]) } func configure(with statuses: [TrackingStatus]) { self.statuses = statuses collectionView.reloadData() } // MARK: - UICollectionView数据源方法 func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return statuses.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TrackingNodeCell", for: indexPath) as! TrackingNodeCell let status = statuses[indexPath.item] cell.configure(with: status) // 最后一个节点不显示连接线 cell.showLine = indexPath.item != statuses.count - 1 return cell } // MARK: - UICollectionView布局方法 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { // 计算文字宽度,确保item能容纳文字 let textWidth = statuses[indexPath.item].title.size(withAttributes: [.font: UIFont.systemFont(ofSize: 12)]).width // 最小宽度设为40,保证圆的布局空间 return CGSize(width: max(40, textWidth + 16), height: collectionView.bounds.height) } }
注意事项
- Cell复用问题:不管用哪个方案,都要在
configure方法里重置状态,避免复用之前的视图或数据 - 样式可配置化:可以把颜色、间距、圆的大小这些做成常量或者可配置的属性,方便后续调整
- 动画优化:如果需要状态切换的动画,可以在
configure方法里给圆的颜色变化加UIView.animate过渡
内容的提问来源于stack exchange,提问作者Ashish P
相关产品推荐
相关产品推荐

