一次搞懂 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!

相关推荐
前端老宋Running1 小时前
一种名为“Webpack 配置工程师”的已故职业—— Vite 与“零配置”的快乐
前端·vite·前端工程化
用户6600676685391 小时前
从“养猫”看懂JS面向对象:原型链与Class本质拆解
前端·javascript·面试
parade岁月1 小时前
我的第一个 TDesign PR:修复 Empty 组件的 v-if 警告
前端
云鹤_1 小时前
【Amis源码阅读】低代码如何实现交互(下)
前端·低代码·架构
之恒君1 小时前
JavaScript 对象相等性判断详解
前端·javascript
dhdjjsjs1 小时前
Day30 Python Study
开发语言·前端·python
T___T1 小时前
通过 MCP 让 AI 读懂你的 Figma 设计稿
前端·人工智能
清妍_1 小时前
踩坑记录:Taro.createSelectorQuery找不到元素
前端
爬山算法1 小时前
Redis(169)如何使用Redis实现数据同步?
前端·redis·bootstrap