如何实现UICollectionView首分区居左、次分区居右的双列布局
当然有可行的实现方法!我给你两种思路,一种是用单个UICollectionView+自定义布局来实现,另一种是用两个并排的UICollectionView作为替代方案,都能满足你的需求:
方案一:自定义UICollectionViewLayout(单CollectionView实现)
这是最贴合你需求的方式,通过自定义布局来控制两个分区的位置:
核心思路:
- 将屏幕宽度平均分成两部分,第一个分区的所有元素(包括头部)占据左半屏,第二个分区占据右半屏。
- 重写布局类的关键方法,为每个分区的Cell和头部单独计算布局属性。
代码示例:
首先创建自定义布局类:class DualColumnLayout: UICollectionViewLayout { private var cache = [UICollectionViewLayoutAttributes]() private var contentHeight: CGFloat = 0 private let columnWidth: CGFloat private let spacing: CGFloat = 8 override init() { let screenWidth = UIScreen.main.bounds.width columnWidth = (screenWidth - spacing) / 2 super.init() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override var collectionViewContentSize: CGSize { return CGSize(width: UIScreen.main.bounds.width, height: contentHeight) } override func prepare() { super.prepare() guard let collectionView = collectionView else { return } cache.removeAll() contentHeight = 0 // 处理第一个分区(左列) let leftSection = 0 let leftSectionInset = collectionView.insetsForSection(at: leftSection) var leftYOffset = leftSectionInset.top // 添加左分区头部 let leftHeaderAttrs = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: IndexPath(item: 0, section: leftSection)) leftHeaderAttrs.frame = CGRect(x: leftSectionInset.left, y: leftYOffset, width: columnWidth - leftSectionInset.left - leftSectionInset.right, height: 50) // 自定义头部高度 cache.append(leftHeaderAttrs) leftYOffset += leftHeaderAttrs.frame.height + leftSectionInset.top // 布局左分区Cell let leftItemCount = collectionView.numberOfItems(inSection: leftSection) for item in 0..<leftItemCount { let indexPath = IndexPath(item: item, section: leftSection) let attrs = UICollectionViewLayoutAttributes(forCellWith: indexPath) attrs.frame = CGRect(x: leftSectionInset.left, y: leftYOffset, width: columnWidth - leftSectionInset.left - leftSectionInset.right, height: 100) // 自定义Cell高度 cache.append(attrs) leftYOffset += attrs.frame.height + spacing } contentHeight = max(contentHeight, leftYOffset) // 处理第二个分区(右列) let rightSection = 1 let rightSectionInset = collectionView.insetsForSection(at: rightSection) var rightYOffset = rightSectionInset.top // 添加右分区头部 let rightHeaderAttrs = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: IndexPath(item: 0, section: rightSection)) rightHeaderAttrs.frame = CGRect(x: columnWidth + spacing + rightSectionInset.left, y: rightYOffset, width: columnWidth - rightSectionInset.left - rightSectionInset.right, height: 50) cache.append(rightHeaderAttrs) rightYOffset += rightHeaderAttrs.frame.height + rightSectionInset.top // 布局右分区Cell let rightItemCount = collectionView.numberOfItems(inSection: rightSection) for item in 0..<rightItemCount { let indexPath = IndexPath(item: item, section: rightSection) let attrs = UICollectionViewLayoutAttributes(forCellWith: indexPath) attrs.frame = CGRect(x: columnWidth + spacing + rightSectionInset.left, y: rightYOffset, width: columnWidth - rightSectionInset.left - rightSectionInset.right, height: 100) cache.append(attrs) rightYOffset += attrs.frame.height + spacing } contentHeight = max(contentHeight, rightYOffset) } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return cache.filter { $0.frame.intersects(rect) } } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { return cache.first { $0.indexPath == indexPath } } override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { return cache.first { $0.indexPath == indexPath && $0.representedElementKind == elementKind } } override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true // 适配屏幕旋转 } }然后在ViewController中设置:
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate { private var collectionView: UICollectionView! override func viewDidLoad() { super.viewDidLoad() let layout = DualColumnLayout() collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView.dataSource = self collectionView.delegate = self collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell") collectionView.register(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "Header") view.addSubview(collectionView) } // 实现数据源方法 func numberOfSections(in collectionView: UICollectionView) -> Int { return 2 } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return section == 0 ? 5 : 7 // 示例数量 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) cell.backgroundColor = indexPath.section == 0 ? .systemBlue : .systemGreen return cell } func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) header.backgroundColor = .systemGray // 添加标题标签等 let label = UILabel(frame: header.bounds) label.text = indexPath.section == 0 ? "左分区头部" : "右分区头部" label.textAlignment = .center header.addSubview(label) return header } }
方案二:两个并排的UICollectionView(替代方案)
如果自定义布局觉得太复杂,这个方案更简单易维护:
- 核心思路:在父视图中放置两个UICollectionView,左边的对应原第一个分区,右边的对应原第二个分区,用AutoLayout让它们各占屏幕宽度的一半(可调整间距)。
- 优点:每个CollectionView可以直接使用系统的
UICollectionViewFlowLayout,单独设置自己的分区头部,无需复杂的自定义布局代码,适配和调试更方便。
代码示例:
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { private var leftCollectionView: UICollectionView! private var rightCollectionView: UICollectionView! override func viewDidLoad() { super.viewDidLoad() // 创建左CollectionView let leftFlowLayout = UICollectionViewFlowLayout() leftFlowLayout.sectionHeadersPinToVisibleBounds = true // 可选:头部吸顶 leftCollectionView = UICollectionView(frame: .zero, collectionViewLayout: leftFlowLayout) leftCollectionView.dataSource = self leftCollectionView.delegate = self leftCollectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "LeftCell") leftCollectionView.register(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "LeftHeader") leftCollectionView.backgroundColor = .systemBackground view.addSubview(leftCollectionView) // 创建右CollectionView let rightFlowLayout = UICollectionViewFlowLayout() rightFlowLayout.sectionHeadersPinToVisibleBounds = true rightCollectionView = UICollectionView(frame: .zero, collectionViewLayout: rightFlowLayout) rightCollectionView.dataSource = self rightCollectionView.delegate = self rightCollectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "RightCell") rightCollectionView.register(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "RightHeader") rightCollectionView.backgroundColor = .systemBackground view.addSubview(rightCollectionView) // 设置AutoLayout leftCollectionView.translatesAutoresizingMaskIntoConstraints = false rightCollectionView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ leftCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), leftCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), leftCollectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), leftCollectionView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5, constant: -4), // 4是间距的一半 rightCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), rightCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), rightCollectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), rightCollectionView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5, constant: -4), rightCollectionView.leadingAnchor.constraint(equalTo: leftCollectionView.trailingAnchor, constant: 8) // 间距8 ]) } // 数据源方法区分两个CollectionView func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 // 每个CollectionView对应一个分区 } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return collectionView == leftCollectionView ? 5 : 7 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: collectionView == leftCollectionView ? "LeftCell" : "RightCell", for: indexPath) cell.backgroundColor = collectionView == leftCollectionView ? .systemBlue : .systemGreen return cell } func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: collectionView == leftCollectionView ? "LeftHeader" : "RightHeader", for: indexPath) header.backgroundColor = .systemGray let label = UILabel(frame: header.bounds) label.text = collectionView == leftCollectionView ? "左分区头部" : "右分区头部" label.textAlignment = .center header.addSubview(label) return header } // 设置头部高度 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { return CGSize(width: collectionView.bounds.width, height: 50) } // 设置Cell大小 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: collectionView.bounds.width - 16, height: 100) // 左右边距各8 } }
注意事项
- 两种方案都要考虑屏幕旋转适配:自定义布局重写
shouldInvalidateLayout(forBoundsChange:)返回true;双CollectionView方案AutoLayout会自动适配。 - 可以根据需求调整间距、头部高度、Cell高度等参数。
- 如果需要分区头部吸顶效果,自定义布局需要额外处理,双CollectionView方案直接设置
sectionHeadersPinToVisibleBounds = true即可。
内容的提问来源于stack exchange,提问作者Hari R Krishna
相关产品推荐
相关产品推荐

