阅读视图

发现新文章,点击刷新页面。

SwiftUI 中的 compositingGroup():真正含义与渲染原理

在学习 SwiftUI 的过程中,很多人第一次看到 compositingGroup() 都会被官方文档这句话绕晕:

Use compositingGroup() to apply effects to a parent view before applying effects to this view.

“让父 View 的效果先于子 View 的效果生效”  —— 这句话如果按字面理解,几乎一定会误解。

本文将从 渲染顺序、效果作用范围、实际示例 三个角度,彻底讲清楚 compositingGroup() 到底解决了什么问题。


一句话结论(先记住)

compositingGroup() 会创建一个“合成边界”:

  • 没有它:父 View 的合成效果会被「拆分」并逐个作用到子 View
  • 有了它:子 View 会先整体合成,再统一应用父 View 的合成效果

⚠️ 它改变的不是 modifier 的书写顺序,而是“效果的作用范围”。


SwiftUI 默认的渲染行为(最关键)

先看一个最简单的例子:

VStack {
    Text("A")
    Text("B")
}
.opacity(0.5)

看起来是对 VStack 设置了透明度

但 SwiftUI 实际做的是:

Text("A") -> opacity 0.5
Text("B") -> opacity 0.5
再进行叠加

也就是说:

  • opacity 并没有作为一个“整体效果”存在
  • 而是被 拆分后逐个应用到子 View

这就是很多「透明度叠加变脏」「blur 看起来不对劲」的根源。


compositingGroup() 做了什么?

加上 compositingGroup()

VStack {
    Text("A")
    Text("B")
}
.compositingGroup()
.opacity(0.5)

SwiftUI 的渲染流程会变成:

VStack
 ├─ Text("A")
 └─ Text("B")

先合成为一张离屏图像

对这张图像应用 opacity 0.5

关键变化只有一句话

父 View 的合成类效果不再下发到子 View。


那官方说的“父 View 的效果先于子 View 的效果”是什么意思?

这句话并不是时间顺序,而是:

父 View 的合成效果不会参与子 View 的内部计算。

换句话说:

  • 子 View 内部的 blur / color / mask 先完成
  • 父 View 的 opacity / blendMode 再整体生效

而不是交叉、叠加、重复计算。


一个典型示例:blur + opacity

❌ 没有 compositingGroup

ZStack {
    Text("Hello")
    Text("Hello")
        .blur(radius: 5)
}
.opacity(0.5)

实际效果:

  1. 第二个 Text 先 blur
  2. 两个 Text 分别被 opacity 影响
  3. 模糊区域再次参与透明度混合
  4. 结果:画面更糊、更脏

✅ 使用 compositingGroup

ZStack {
    Text("Hello")
    Text("Hello")
        .blur(radius: 5)
}
.compositingGroup()
.opacity(0.5)

渲染流程变为:

  1. 子 View 内部:blur 只影响指定的 Text
  2. ZStack 合成完成
  3. 整体统一 opacity 0.5

📌 blur 不再被“二次污染”


compositingGroup() 常见适用场景

1️⃣ 半透明容器(避免透明度叠加)

VStack {
    ...
}
.compositingGroup()
.opacity(0.8)

2️⃣ blendMode 视觉异常

ZStack {
    ...
}
.compositingGroup()
.blendMode(.multiply)

3️⃣ 动画 + blur / scale / opacity

.content
.compositingGroup()
.transition(.opacity)

可显著减少闪烁、重影问题。


compositingGroup vs drawingGroup

对比项 compositingGroup drawingGroup
是否离屏渲染
是否使用 Metal
主要目的 控制合成效果作用范围 性能 / 特效加速
常见问题 解决视觉叠加 解决复杂绘制性能

📌 compositingGroup 关注“视觉正确性”,drawingGroup 更偏向“性能”。


记忆口诀(非常实用)

要“整体效果”,用 compositingGroup;
不想被子 View 叠加污染,也用 compositingGroup。


总结

  • compositingGroup() 并不会改变 modifier 的书写顺序
  • 它创建了一个 合成边界(compositing boundary)
  • 阻止父 View 的合成效果被拆分并下发到子 View
  • 在 opacity、blur、blendMode、动画场景中极其重要

如果你在 SwiftUI 中遇到:

  • 透明度看起来“不对”
  • blur 过重
  • 动画时出现重影

👉 第一时间就该想到 compositingGroup()


希望这篇文章能帮你真正理解 SwiftUI 背后的渲染逻辑。

SwiftUI 中的 @ViewBuilder 全面解析

SwiftUI 中的 @ViewBuilder 全面解析

在 SwiftUI 的世界里,@ViewBuilder 是一个你每天都在用,却可能从未认真了解过的核心机制

很多 SwiftUI 看起来“像写 DSL 一样优雅”的代码,其实都离不开它。

本文将从为什么需要它、它解决了什么问题、如何使用、常见坑点几个维度,系统性地介绍 @ViewBuilder,适合 SwiftUI 初学者到中级开发者 阅读。


一、问题的起点:Swift 只能返回一个值

在 Swift 中,函数或计算属性只能返回一个值

但在 SwiftUI 中,我们却经常写出这样的代码:

var body: some View {
    Text("Hello")
    Image(systemName: "star")
    Button("Tap") { }
}

表面看起来像是“返回了多个 View”,这在普通 Swift 函数里是不可能的

那 SwiftUI 是怎么做到的?

答案就是: @ViewBuilder


二、@ViewBuilder 是什么

@ViewBuilder 是 Swift 的一种 Result Builder(结果构建器)

它的核心职责只有一个:

把多行 View 表达式,组合成一个 View 返回。

你写的代码是这样:

Text("A")
Text("B")
Text("C")

编译器在背后会帮你组合成类似:

TupleView<(Text, Text, Text)>

但这些具体类型对开发者是隐藏的,你只需要关心:

可以像写布局一样写 View,而不是手动拼装结构。


三、为什么你很少看到 @ViewBuilder

因为 SwiftUI 已经帮你加好了。

例如:

struct ContentView: View {
    var body: some View {
        Text("Hello")
        Text("World")
    }
}

实际上等价于:

struct ContentView: View {
    @ViewBuilder
    var body: some View {
        Text("Hello")
        Text("World")
    }
}

👉 body 天生就支持多 View 与条件语法


四、@ViewBuilder 支持哪些能力

1️⃣ 多个 View

@ViewBuilder
var content: some View {
    Text("Title")
    Text("Subtitle")
}

2️⃣ if / else 条件渲染(非常重要)

没有 @ViewBuilder,下面代码是非法的:

func makeView(flag: Bool) -> some View {
    if flag {
        Text("Yes")
    } else {
        Text("No")
    }
}

使用 @ViewBuilder 后:

@ViewBuilder
func makeView(flag: Bool) -> some View {
    if flag {
        Text("Yes")
    } else {
        Text("No")
    }
}

👉 这正是 SwiftUI 条件 UI 渲染的基础能力


3️⃣ 只有 if(没有 else

@ViewBuilder
var body: some View {
    Text("Always Visible")

    if isLogin {
        Text("Welcome")
    }
}

当条件不成立时,SwiftUI 会自动插入一个 EmptyView


4️⃣ switch

@ViewBuilder
func stateView(_ state: LoadState) -> some View {
    switch state {
    case .loading:
        ProgressView()
    case .success:
        Text("Success")
    case .error:
        Text("Error")
    }
}

五、最常见的使用场景

1️⃣ 自定义组件的内容闭包

struct Card<Content: View>: View {
    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        VStack(spacing: 8) {
            content
        }
        .padding()
        .background(.gray.opacity(0.2))
        .cornerRadius(12)
    }
}

使用时:

Card {
    Text("Title")
    Text("Subtitle")
}

👉 这正是 SwiftUI 组件化体验优秀的原因之一。


2️⃣ 模仿系统 API(如 .sheet / .toolbar

func customOverlay<Content: View>(
    @ViewBuilder content: () -> Content
) -> some View {
    overlay {
        content()
    }
}

六、常见坑点(非常容易踩)

❌ 1. 不能写普通逻辑代码

@ViewBuilder
var body: some View {
    let count = 10 // ❌ 编译错误
    Text("(count)")
}

原因是:

@ViewBuilder 只接受 生成 View 的表达式

✅ 正确方式:

var count: Int { 10 }

@ViewBuilder
var body: some View {
    Text("(count)")
}

❌ 2. 不能直接使用 for 循环

@ViewBuilder
var body: some View {
    for i in 0..<3 { // ❌
        Text("(i)")
    }
}

✅ 正确方式:

ForEach(0..<3, id: .self) { i in
    Text("(i)")
}

七、什么时候需要主动使用 @ViewBuilder

当你遇到以下情况时,就该考虑它:

  • 希望一个函数 / 闭包返回 多个 View
  • 需要在返回 View 时使用 if / else / switch
  • 编写 可组合的自定义组件

简单判断法则:

“这个 API 是否应该像 SwiftUI 一样写 UI?”

如果答案是「是」,那基本就需要 @ViewBuilder


八、总结

  • @ViewBuilder 是 SwiftUI 的核心基础设施
  • 它让 Swift 支持 声明式 UI 语法
  • 条件渲染、多 View 组合、本质都依赖它
  • 写组件时,合理使用 @ViewBuilder 能极大提升 API 体验

一句话总结:

没有 @ViewBuilder,就没有今天的 SwiftUI。


如果你觉得这篇文章有帮助,欢迎点赞 / 收藏 / 交流 🙌

后续也可以深入聊:

  • ViewBuilder 源码实现
  • @ViewBuilder 与 @ToolbarContentBuilder 的区别
  • SwiftUI 新数据流(@Observable / @Bindable)下的最佳实践
❌