iOS11+兼容UICollectionView自适配高度折叠Cell布局异常求解
iOS11+兼容UICollectionView可折叠动态高度Cell实现问题
我要实现支持可折叠展开、动态高度Cell的UICollectionView,要求兼容iOS11及以上版本,因此不使用Compositional Layout。动态高度Cell的基础实现逻辑很简单:
核心实现代码
自定义FlowLayout及ViewController代码
final class CustomFlowLayout: UICollectionViewFlowLayout { override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let layoutAttributesObjects = super.layoutAttributesForElements(in: rect)?.map{ $0.copy() } as? [UICollectionViewLayoutAttributes] layoutAttributesObjects?.forEach({ layoutAttributes in if layoutAttributes.representedElementCategory == .cell { if let newFrame = layoutAttributesForItem(at: layoutAttributes.indexPath)?.frame { layoutAttributes.frame = newFrame } } }) return layoutAttributesObjects } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { guard let collectionView = collectionView else { fatalError() } guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else { return nil } layoutAttributes.frame.origin.x = sectionInset.left layoutAttributes.frame.size.width = collectionView.safeAreaLayoutGuide.layoutFrame.width - sectionInset.left - sectionInset.right return layoutAttributes } } class ViewController: UIViewController, CollapsingCellDelegate { let items = [ "Lorem ipsum dolor sit amet.", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Lorem ipsum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", ] private var collapsedSections: [IndexPath] = [] private var collapsedItems: [IndexPath] = [] private lazy var collectionView: UICollectionView = { let layout = CustomFlowLayout() layout.scrollDirection = .vertical layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize return UICollectionView(frame: .zero, collectionViewLayout: layout) }() override func viewDidLoad() { super.viewDidLoad() collectionView.backgroundColor = .black collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.contentInsetAdjustmentBehavior = .never collectionView.showsVerticalScrollIndicator = false collectionView.showsHorizontalScrollIndicator = false collectionView.dataSource = self collectionView.register(CollapsingCell.self, forCellWithReuseIdentifier: CollapsingCell.identifier) view.addSubview(collectionView) NSLayoutConstraint.activate([ collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } // 折叠/展开核心逻辑 func didTriggerCollapseEvent(cell: CollapsingCell) { guard let indexPath = collectionView.indexPath(for: cell) else { return } if collapsedItems.contains(indexPath) { self.collapsedItems.removeAll(where: { $0 == indexPath }) } else { collapsedItems.append(indexPath) } UIView.animate(withDuration: 0.8, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: .curveEaseInOut, animations: { self.collectionView.reloadItems(at: [indexPath]) }) } } extension ViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return items.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let myCell = collectionView.dequeueReusableCell(withReuseIdentifier: CollapsingCell.identifier, for: indexPath) as! CollapsingCell myCell.isCollapsed = collapsedItems.contains(indexPath) myCell.delegate = self myCell.configure(title: "Cell #\(indexPath.row)", contentTitle: items[indexPath.row]) return myCell } }
可折叠CollectionViewCell代码
protocol CollapsingCellDelegate: AnyObject { func didTriggerCollapseEvent(cell: CollapsingCell) } public class CollapsingCell: UICollectionViewCell { var isCollapsed: Bool? { didSet { contentLabel.isHidden = isCollapsed ?? false } } static var identifier: String { String(describing: self) } weak var delegate: CollapsingCellDelegate? private let titleLabel = UILabel() private let collapseButton = UIButton(type: .system) private let stackView = UIStackView() private let contentLabel = UILabel() override init(frame: CGRect) { super.init(frame: frame) setupViews() } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupViews() { contentView.clipsToBounds = true contentView.layer.cornerRadius = 9 contentView.backgroundColor = .white stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical contentView.addSubview(stackView) let headerView = UIView() headerView.translatesAutoresizingMaskIntoConstraints = false titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.textColor = .black titleLabel.font = .systemFont(ofSize: 22, weight: .bold) headerView.addSubview(titleLabel) collapseButton.addTarget(self, action: #selector(collapseButtonTapped), for: .touchUpInside) collapseButton.setTitle("Collapse/Expend", for: .normal) collapseButton.translatesAutoresizingMaskIntoConstraints = false headerView.addSubview(collapseButton) stackView.addArrangedSubview(headerView) contentLabel.numberOfLines = 0 contentLabel.lineBreakMode = .byWordWrapping contentLabel.textColor = .black contentLabel.translatesAutoresizingMaskIntoConstraints = false stackView.addArrangedSubview(contentLabel) NSLayoutConstraint.activate([ titleLabel.topAnchor.constraint(equalTo: headerView.topAnchor, constant: 16), titleLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: 16), titleLabel.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: -16), collapseButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), collapseButton.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 16), stackView.topAnchor.constraint(equalTo: contentView.topAnchor), stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) ]) } @objc func collapseButtonTapped() { delegate?.didTriggerCollapseEvent(cell: self) } func configure(title: String, contentTitle: String) { titleLabel.text = title contentLabel.text = contentTitle } public override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0) let size = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) layoutAttributes.frame.size = size return layoutAttributes } }
现有表现
未进行折叠展开操作时运行正常,Cell内部使用基础StackView,包含Header和内容视图两个部分:点击折叠按钮时,内容视图切换可见性,StackView会同步修改自身尺寸进而改变Cell尺寸,仅需要刷新对应Cell即可完成状态更新。
存在问题
列表前几个Cell折叠功能正常,操作列表底部的Cell折叠时,UICollectionView会出现布局错乱。
已尝试修改animateWithDuration代码块、在performBatchUpdates块内执行刷新指定item/刷新整个列表/invalidateLayout操作,但最好的结果也只是出现其他异常bug,未解决核心问题。
内容的提问来源于stack exchange,提问作者Serjio
相关产品推荐
相关产品推荐

