Swift实现:添加元素时动态调整UICollectionViewCell布局宽度
Hey there! 作为Swift新手想要实现这种循环式的UICollectionView动态布局,我来给你梳理一个清晰的解决方案~
核心实现思路
这种非标准的流式布局,系统自带的UICollectionViewFlowLayout很难满足需求,我们需要自定义一个UICollectionViewLayout,根据元素总数量对3取模的结果,来切换三种布局模式(1个、2个、3个元素的布局),然后逐个计算每个cell的frame即可。
第一步:自定义布局类的基础结构
先创建一个继承自UICollectionViewLayout的类,定义布局类型枚举和必要属性:
enum LayoutType { case singleItem // 1个元素的布局 case twoItems // 2个元素的布局 case threeItems // 3个元素的布局 } class CustomCycleLayout: UICollectionViewLayout { // 缓存所有cell的布局属性,避免重复计算 private var layoutAttributesCache = [UICollectionViewLayoutAttributes]() // 布局内容的总宽度和高度 private var contentWidth: CGFloat = 0 private var contentHeight: CGFloat = 0 // 根据元素总数判断当前应该使用哪种布局 private var currentLayoutType: LayoutType { guard let itemCount = collectionView?.numberOfItems(inSection: 0) else { return .singleItem } let remainder = itemCount % 3 switch remainder { case 1: return .singleItem case 2: return .twoItems default: // 余数为0时,对应3个元素的布局 return .threeItems } } // 返回布局的整体尺寸 override var collectionViewContentSize: CGSize { return CGSize(width: contentWidth, height: contentHeight) } }
第二步:计算每个cell的布局位置
重写prepare()方法,在这里根据当前布局类型,逐个计算每个cell的frame并缓存:
override func prepare() { super.prepare() layoutAttributesCache.removeAll() guard let collectionView = collectionView else { return } contentWidth = collectionView.bounds.width let spacing: CGFloat = 8 // 元素间的间距,可根据设计调整 let itemCount = collectionView.numberOfItems(inSection: 0) switch currentLayoutType { case .singleItem: // 单个元素:占满宽度,高度设为宽度的0.75(可自定义比例) let itemWidth = contentWidth - spacing * 2 let itemHeight = itemWidth * 0.75 let frame = CGRect(x: spacing, y: spacing, width: itemWidth, height: itemHeight) let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: 0, section: 0)) attributes.frame = frame layoutAttributesCache.append(attributes) contentHeight = itemHeight + spacing * 2 case .twoItems: // 两个元素:左右并排,宽度各占一半减间距 let itemWidth = (contentWidth - spacing * 3) / 2 let itemHeight = itemWidth * 0.75 // 第一个cell let firstFrame = CGRect(x: spacing, y: spacing, width: itemWidth, height: itemHeight) let firstAttributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: 0, section: 0)) firstAttributes.frame = firstFrame layoutAttributesCache.append(firstAttributes) // 第二个cell let secondFrame = CGRect(x: itemWidth + spacing * 2, y: spacing, width: itemWidth, height: itemHeight) let secondAttributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: 1, section: 0)) secondAttributes.frame = secondFrame layoutAttributesCache.append(secondAttributes) contentHeight = itemHeight + spacing * 2 case .threeItems: // 三个元素:顶部一个占满宽度,下方两个左右并排 // 顶部cell let topItemWidth = contentWidth - spacing * 2 let topItemHeight = topItemWidth * 0.5 let topFrame = CGRect(x: spacing, y: spacing, width: topItemWidth, height: topItemHeight) let topAttributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: 0, section: 0)) topAttributes.frame = topFrame layoutAttributesCache.append(topAttributes) // 下方两个cell let bottomItemWidth = (contentWidth - spacing * 3) / 2 let bottomItemHeight = bottomItemWidth * 0.75 let bottomY = topItemHeight + spacing * 2 // 第二个cell let secondFrame = CGRect(x: spacing, y: bottomY, width: bottomItemWidth, height: bottomItemHeight) let secondAttributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: 1, section: 0)) secondAttributes.frame = secondFrame layoutAttributesCache.append(secondAttributes) // 第三个cell let thirdFrame = CGRect(x: bottomItemWidth + spacing * 2, y: bottomY, width: bottomItemWidth, height: bottomItemHeight) let thirdAttributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: 2, section: 0)) thirdAttributes.frame = thirdFrame layoutAttributesCache.append(thirdAttributes) contentHeight = bottomY + bottomItemHeight + spacing } }
第三步:返回布局属性给CollectionView
重写两个方法,返回我们缓存的布局属性:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { var visibleAttributes = [UICollectionViewLayoutAttributes]() for attributes in layoutAttributesCache { if attributes.frame.intersects(rect) { visibleAttributes.append(attributes) } } return visibleAttributes } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { return layoutAttributesCache[indexPath.item] }
第四步:在ViewController中使用自定义布局
最后在你的页面里,初始化CollectionView时使用这个自定义布局即可:
class DemoViewController: UIViewController, UICollectionViewDataSource { private var collectionView: UICollectionView! private var dataSource = [String]() // 你的数据源 override func viewDidLoad() { super.viewDidLoad() setupUI() } private func setupUI() { // 添加添加元素的按钮 let addBtn = UIButton(type: .system) addBtn.setTitle("添加元素", for: .normal) addBtn.addTarget(self, action: #selector(addNewItem), for: .touchUpInside) addBtn.frame = CGRect(x: 20, y: view.safeAreaInsets.top + 20, width: view.bounds.width - 40, height: 44) view.addSubview(addBtn) // 初始化CollectionView let customLayout = CustomCycleLayout() collectionView = UICollectionView(frame: CGRect(x: 0, y: view.safeAreaInsets.top + 80, width: view.bounds.width, height: view.bounds.height - view.safeAreaInsets.top - 80), collectionViewLayout: customLayout) collectionView.dataSource = self collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "CycleCell") collectionView.backgroundColor = .white view.addSubview(collectionView) } @objc private func addNewItem() { dataSource.append("Item \(dataSource.count + 1)") collectionView.reloadData() } // MARK: - UICollectionViewDataSource func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return dataSource.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CycleCell", for: indexPath) cell.backgroundColor = .systemTeal // 添加文本标签 if let label = cell.viewWithTag(100) as? UILabel { label.text = dataSource[indexPath.item] } else { let label = UILabel(frame: cell.bounds) label.tag = 100 label.textAlignment = .center label.textColor = .white label.font = .systemFont(ofSize: 16) cell.addSubview(label) label.text = dataSource[indexPath.item] } return cell } }
可调整的细节
- 所有的间距、元素宽高比例都可以根据你的设计图修改,比如单个元素的高度、三个元素时顶部元素的高度等。
- 如果你的需求是每3个元素为一组循环布局(比如4个元素是3个的布局+1个的布局,5个是3+2,6个是3+3),只需要修改
prepare()方法,循环计算每组的位置并累加contentHeight即可,逻辑类似上面的单组计算。
内容的提问来源于stack exchange,提问作者Duffy DD
相关产品推荐
相关产品推荐

