一次搞懂 iOS 组合布局:用 CompositionalLayout 打造马赛克 + 网格瀑布流

一次搞懂 iOS 组合布局:用 CompositionalLayout 打造马赛克 + 网格瀑布流


最近在项目中遇到一个很有趣的需求:
要实现一个"左边大图 + 右边 2×2 小图"的马赛克布局,下面再接一个三列网格瀑布流。

乍一看,有点像短视频 App 的推荐页或者流媒体内容流。

以往这种复杂布局,我们可能会:

  • 写自定义 FlowLayout
  • 手动算每个 cell 的 frame
  • 各种 if else 填满屏幕
  • 容易出错且维护难度大

但今天我才发现 ------ UICollectionViewCompositionalLayout 能把这种布局写得既优雅又清晰!


🎯 需求拆解

视觉结构如下:

总结:

  • Section 0:前 5 个元素

    • 左侧:1 个大图(宽度 50%,高度与右侧一致)
    • 右侧:4 个小图(2 × 2)
  • Section 1:剩余元素进入 3 列网格布局

适用场景:

  • 首页推荐流
  • 杂志式内容展示
  • 多风格 Feed 流
  • Banner + 内容区结合布局

🧠 为什么用 UICollectionViewCompositionalLayout?

Apple 官方定位:
Compositional Layout = 用组合方式表达复杂布局的声明式系统

优势:

  1. 完全声明式:不用操心 layoutAttributes
  2. 多 Section 自由组合:每个 section 可以完全不同
  3. 适合不规则布局:左右不对称、嵌套 group、水平 + 垂直混合滚动

对于我们的"马赛克区 + 网格区"需求,简直天生适合。


📐 核心知识点(深入版)

1️⃣ Item(最基本元素)

Item 就是 UICollectionView 中的 cell 对应的布局单元

它不仅控制单个 cell 的尺寸,还能设置间距、对齐方式。

关键属性

  • widthDimension / heightDimension

    • .fractionalWidth(0.5):相对于父 Group 宽度的一半
    • .fractionalHeight(1.0):填满父 Group 高度
    • .absolute(100):固定尺寸
    • .estimated(150):动态高度,适合自适应内容
  • contentInsets

    • 控制 cell 与 cell 之间的间距
    • 例如:NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
    • 避免手动计算 frame,也能保证布局美观

💡 Tip:在嵌套 Group 中,Item 的 fractional 高度是相对于 直接父 Group 的高度


2️⃣ Group(组合 Item 的容器)

Group 是 把若干 Item 或 Group 组合起来的容器 ,可以是水平(horizontal)或垂直(vertical)。

理解 Group 是理解 CompositionalLayout 的关键。

核心概念

  1. 水平 Group

    • 将多个 Item 横向排列

    • 例如右侧 2 个小格子在同一行:

      less 复制代码
      NSCollectionLayoutGroup.horizontal(layoutSize: rowSize, subitem: smallItem, count: 2)
  2. 垂直 Group

    • 将多个 Item 或水平 Group 纵向叠加

    • 例如右侧 2x2 小图:

      • 先把一行两个小图组合成水平 group
      • 再把上下两行叠加成垂直 group
  3. 嵌套 Group

    • 左右布局:把左侧大图和右侧小图列组合成一个水平 Group
    • 核心思路:从最小的元素开始组合,逐层嵌套,最终形成完整 Section

💡 Tip:Group 的尺寸也是 fractional,相对父 Group 或 Section,保证屏幕适配。


3️⃣ Section(整个区域)

Section 是 最终展示区域,包含一个或多个 Group。

Section 的作用

  • 定义整个布局区域的 边距、间距

    • section.contentInsets 控制整个区域与 CollectionView 边界的距离
  • 可以为 Section 添加 Header / Footer

  • Section 可以自由组合,形成多风格布局

直观理解:

  • Item = 一张卡片
  • Group = 小版面 / 组合卡片
  • Section = 整个版块

4️⃣ 布局比例理解技巧

  1. 用 fractionalWidth/fractionalHeight 控制比例

    • 宽高比例可以随屏幕自适应
    • 保证布局在 iPhone / iPad 上一致
  2. 嵌套顺序

    • 先从最小的 Item 开始设置尺寸
    • 再组合成水平 / 垂直 Group
    • 最后组合成 Section
    • 这样思路清晰,容易调整
  3. 间距管理

    • Item.contentInsets 控制 cell 内间距
    • Section.contentInsets 控制整体区域边距
    • 避免在 Group 中重复设置过多间距

5️⃣ 总结思路

如果用一句话概括 CompositionalLayout 的核心逻辑:

"先定义每个 Item,再组合成 Group,最后把 Group 放进 Section,层层嵌套,形成最终布局。"

这个思路放到我们的示例里就是:

  1. 左侧大图 → 一个 Item
  2. 右侧小图 → 小 Item → 水平 Row Group → 垂直 Column Group
  3. 主组 = 左大图 + 右侧 Column Group
  4. Section 0 = 主组 + contentInsets
  5. Section 1 = 三列网格

🛠 实战:完整 createCompositionalLayout() 方法

less 复制代码
private func createCompositionalLayout() -> UICollectionViewLayout {
    return UICollectionViewCompositionalLayout { [weak self] (sectionIndex, env) -> NSCollectionLayoutSection? in
        guard let self = self else { return nil }
        
        let spacing: CGFloat = 4.0
        
        // Section 0:马赛克区 (左大右 2x2)
        if sectionIndex == 0 {
            
            let leftLargeItemSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(0.5),
                heightDimension: .fractionalHeight(1.0)
            )
            let leftLargeItem = NSCollectionLayoutItem(layoutSize: leftLargeItemSize)
            leftLargeItem.contentInsets = .init(top: spacing, leading: spacing, bottom: spacing, trailing: spacing)
            
            let smallItemSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(0.5),
                heightDimension: .fractionalHeight(1.0)
            )
            let smallItem = NSCollectionLayoutItem(layoutSize: smallItemSize)
            smallItem.contentInsets = .init(top: spacing, leading: spacing, bottom: spacing, trailing: spacing)
            
            let rightRowGroup = NSCollectionLayoutGroup.horizontal(
                layoutSize: NSCollectionLayoutSize(
                    widthDimension: .fractionalWidth(1.0),
                    heightDimension: .fractionalHeight(0.5)
                ),
                subitem: smallItem,
                count: 2
            )
            
            let rightColumnGroup = NSCollectionLayoutGroup.vertical(
                layoutSize: NSCollectionLayoutSize(
                    widthDimension: .fractionalWidth(0.5),
                    heightDimension: .fractionalHeight(1.0)
                ),
                subitem: rightRowGroup,
                count: 2
            )
            
            let mainGroup = NSCollectionLayoutGroup.horizontal(
                layoutSize: NSCollectionLayoutSize(
                    widthDimension: .fractionalWidth(1.0),
                    heightDimension: .fractionalWidth(0.5)
                ),
                subitems: [leftLargeItem, rightColumnGroup]
            )
            
            let section = NSCollectionLayoutSection(group: mainGroup)
            section.contentInsets = .init(top: spacing, leading: spacing, bottom: spacing, trailing: spacing)
            return section
        }
        
        // Section 1:下方三列网格
        else {
            let itemSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0/3.0),
                heightDimension: .fractionalHeight(1.0)
            )
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            item.contentInsets = .init(top: spacing, leading: spacing, bottom: spacing, trailing: spacing)
            
            let groupSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: .fractionalWidth(1.0/3.0)
            )
            let group = NSCollectionLayoutGroup.horizontal(
                layoutSize: groupSize,
                subitem: item,
                count: 3
            )
            
            let section = NSCollectionLayoutSection(group: group)
            section.contentInsets = .init(top: 0, leading: spacing, bottom: spacing, trailing: spacing)
            return section
        }
    }
}

✅ 完整实现了"左大右 2×2 + 下方三列网格",保持比例、间距和嵌套逻辑。


🔄 数据分区逻辑

前 5 个元素给 Section 0,剩下的给 Section 1。

swift 复制代码
func numberOfSections(in collectionView: UICollectionView) -> Int {
    return items.isEmpty ? 0 : 2
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return section == 0 ? min(5, items.count) : max(0, items.count - 5)
}

数据分区 + CompositionalLayout,就能轻松实现复杂瀑布流效果。


✅ 总结

适用场景:

  • 杂志排版
  • 视频 / 图片内容流
  • 商品推荐页
  • Banner + 宫格混合布局

优势:

  • 结构清晰
  • 易维护
  • 代码优雅
  • UIKit 下的现代布局方式

如果你还在用 FlowLayout 手动计算 frame,真的要试试 CompositionalLayout!

相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax