← 所有文章

SwiftUI由什么构成

类型: 框架解析。本文阐述SwiftUI所依赖的底层基质:result builders、不透明返回类型,以及值类型的视图树。一旦这层基质变得可见,SwiftUI中那些让开发者意外的部分(AnyViewGroupViewBuilder@ViewBuilder参数,以及那个令人头疼的some View vs any View错误)就不再神秘。

SwiftUI视图是一种值类型,它遵循一个仅有单一要求的协议。框架的其余部分都建立在SwiftUI之外的Swift语言特性之上:result builders、不透明类型、带约束的泛型、属性包装器。如果您理解这些语言特性,框架读起来就像普通的Swift API。如果不理解,框架读起来就像偶尔会反咬一口的魔法。

本文将逐步剖析这层底层基质。这里没有LiveActivityManager,也没有Get Bananas的截图。重点在于框架本身,而非某个项目;一旦框架变得清晰可读,该系列中所有展示已交付代码的文章读起来都会更加流畅。

TL;DR

  • SwiftUI视图是遵循View协议的Swift值类型。该协议只有一个要求:var body: some View { get }。其余一切都建立在Swift语言特性之上。
  • @ViewBuilder是一种result builder。每个View的body都是一个result builder。Result builders通过编译器合成的方法调用,将以逗号分隔的表达式转换为单一的返回值。
  • some View是一种不透明返回类型。编译器知道具体类型;调用方则不知道。这种不透明类型正是视图body在编译时和运行时都能保持高效的关键;AnyView则是当不透明性失效时,作为类型擦除的逃生出口。
  • GroupEmptyViewTupleView_ConditionalContent是result builders合成的实现类型。它们有完整文档,但很少需要手写。

一切始于的协议

View协议只有一个要求:1

public protocol View {
    associatedtype Body : View
    @ViewBuilder var body: Self.Body { get }
}

理解SwiftUI的其余部分,需要关注该协议的两个要点。

关联类型Body : View 视图的body本身也是一个视图。这种递归结构正是框架可组合性的根源。每个View的body返回另一个View,如此层层嵌套,直到链条在框架的某个基本视图(例如TextColorImageEmptyView,它们的BodyNever)处终止。基本视图是树的叶节点;您编写的视图则是分支。

body上的@ViewBuilder属性。 每个body都是一个result-builder闭包。Result builders是Swift语言特性,在SE-0289中有详细记载(在Swift 5.4中正式确立为@resultBuilder),它允许编译器通过合成方法调用,将含有一系列表达式的闭包转换为单一返回值。2 这种转换正是SwiftUI body内部那种无逗号、语句式语法得以成立的原因。

这种协议形态之所以特殊,有两个原因。

第一,该要求是一个计算属性,而非方法。当SwiftUI判定视图状态发生变化时,视图的body会在每次渲染过程中重新计算。框架将body视为调用成本低廉的操作;在body内部进行长时间计算属于反模式,因为它会在每次渲染时执行。

第二,Self.Body是关联类型,而非被擦除的类型。视图的具体body类型在编译时就是其签名的一部分。Text("Hello")的body类型是Never;自定义视图的body类型则是@ViewBuilder为其body所合成的任何类型。这种关联类型设计使编译器能够在不进行运行时类型检查的情况下优化视图树。同时,这也是当自定义视图返回条件内容时产生some View要求的根本原因。

Result Builders:无逗号的DSL

Result builder是一种Swift语言特性,它通过插入编译器合成的方法调用,将闭包转换为单一返回值。@ViewBuilder就是一种result builder。每个SwiftUI视图的body都是它的闭包。2

请看这个视图:

struct ExampleView: View {
    var body: some View {
        Text("Title")
        Text("Subtitle")
        Image(systemName: "star")
    }
}

body包含三条没有分隔符的语句。在普通Swift中,这是编译错误:闭包只能返回一个值。Result builders会在编译前重写闭包。经过@ViewBuilder展开后,编译器实际看到的代码大致是:

struct ExampleView: View {
    var body: some View {
        ViewBuilder.buildBlock(
            Text("Title"),
            Text("Subtitle"),
            Image(systemName: "star")
        )
    }
}

ViewBuilder.buildBlock(_:_:_:)是一个静态方法,它接收三个视图并返回一个TupleView<(Text, Text, Image)>。body返回的就是这个单一的tuple-view值。早期SwiftUI提供了固定数量的buildBlock重载,分别对应1、2、3…直到10个子视图;而当前版本的SwiftUI使用Swift的可变泛型支持(buildBlock<each Content>),因此包含11个或更多兄弟视图的body不再是特例。

同样的模式也处理控制流。一个含有if语句的视图body如下所示:

struct ConditionalView: View {
    let isActive: Bool
    var body: some View {
        if isActive {
            Text("Active")
        } else {
            Text("Inactive")
        }
    }
}

@ViewBuilder通过buildEither(first:) / buildEither(second:)调用对其进行重写,生成_ConditionalContent<Text, Text>。即使在任意一次渲染中只有一个分支被执行,编译器在编译时就已经知道结果类型。

if letswitch、可选解包以及其他若干结构,均由result builder的各种buildXxx静态方法处理。3 重复内容则是该语言特性通过buildArray支持但@ViewBuilder并不支持的一个显著情况:在body内部直接使用for循环会失败,报错为“closure containing control flow statement cannot be used with result builder ‘ViewBuilder’.” 符合SwiftUI风格的解决方案是ForEach,它接收一个RandomAccessCollection和一个内容闭包,并将迭代过程合成为单一的值类型视图。这套DSL并非定制化产物;它就是Swift result builders,只是为视图做了配置。

some View与不透明性问题

自定义视图的body通常返回some View。这个关键字称为不透明返回类型,在Swift 5.1中加入。4

some View的含义是:”我返回一个遵循View的特定类型,但我不告诉您是哪个类型。” 编译器在内部跟踪具体类型以进行优化;视图的调用方只能看到协议见证。这种模式让视图的body可以返回像VStack<TupleView<(Text, Image, Spacer)>>这样复杂的类型,而无需在源代码中写出该类型。

关于some View,有两点常常让SwiftUI新手感到困惑:

some View是一个特定类型,即使您返回的是不同的内容。 表达式if condition { Text("A") } else { Image("b") }@ViewBuilder body内部是允许的,因为result builder将两个分支都包装在_ConditionalContent中,从而产生单一的具体类型。但表达式if condition { return Text("A") } else { return Image("b") }在result builder之外则是编译错误:两个分支返回不同的具体类型,而some View只允许一个。Result builders正是让条件返回形态得以工作的关键;显式的return则会丢失result-builder的转换。

some Viewany View不同。 some View是不透明的(一个隐藏的特定类型);any View是存在类型(一个可以容纳任何遵循类型的盒子,带有运行时开销)。一个自由函数或属性可以合法地返回any View。然而,View协议的body不能:协议要求associatedtype Body: View,而any View本身并不遵循View,因此var body: any View无法满足该协议,编译器会建议使用some View。实用规则是:视图body使用some View,运行时变化的视图类型则求助于AnyView(类型擦除的包装器)。错误信息“function declares an opaque return type but the return statements in its body do not have matching underlying types”几乎总是意味着您试图从some View函数中返回不同的具体类型,这时您需要使用result-builder分支或AnyView

AnyView:逃生出口

AnyView是一个类型擦除的视图包装器。构造方式是AnyView(myView)。该包装器可以容纳任何遵循View的视图,SwiftUI在期望View的位置都会接受它。5

逃生出口的使用场景是:某个函数根据运行时数据返回不同的具体类型,且无法通过result-builder分支表达:

func viewForKind(_ kind: Kind) -> AnyView {
    switch kind {
    case .text: return AnyView(Text("hello"))
    case .image: return AnyView(Image("photo"))
    case .custom: return AnyView(MyCustomView())
    }
}

使用AnyView的代价在于:底层类型不再是视图静态身份的一部分。Apple的官方文档对此后果有明确说明:当AnyView内部包装的类型在不同渲染之间发生变化时,现有的视图层级会被销毁,并在原位创建一个新的层级,这意味着状态丢失、动画重启和身份丢失。重新包装相同的具体类型不会触发这种销毁,但无论如何,框架所偏好的基于静态类型的差异比对都不再可用。

正确的规则是:对于条件视图(ifswitchfor),优先使用@ViewBuilder result-builder分支;对于变化的类型,优先使用参数化视图;只有在两者都不奏效时才求助于AnyView。一个返回AnyViewfunc viewForKind通常意味着您应该让viewForKind返回some View,并把switch放进result-builder闭包内部。

GroupEmptyViewTupleView:实现类型

Result builder会合成特定的具体视图类型。其中三个值得认识:6

Group是一个透明容器。它最多接收十个视图作为内容,并将它们作为兄弟节点呈现给父布局。容器本身不增加任何视觉结构;内容的渲染方式与单独渲染时完全相同。其使用场景是在期望单一视图的上下文中包装多个视图(.if修饰符、条件返回、产生”一个视图”的函数)。Group { Text("A"); Text("B") }是一个包含两个视图的单一视图;它是result builders隐式做的事情的显式形式。

EmptyView是一个不渲染任何内容的视图。当if没有else时,result builder会用它作为条件假分支。在您自己的代码中返回EmptyView(),是一种在不改变函数返回类型的前提下选择不渲染的方式。

TupleView是当body包含多个兄弟视图时result builders产生的具体类型。本文开头那个返回三个兄弟视图的表达式,实际上返回的是TupleView<(Text, Text, Image)>。您几乎从不需要直接编写TupleView;您只会在错误信息中读到它。

_ConditionalContent(带前导下划线)是处理if/else分支的类型。该类型出现在ViewBuilder的公开接口中,但带下划线的命名暗示”不要轻易直接使用”;让result builder从if/else合成它,而不要手动构造。

在自己的函数上使用@ViewBuilder

Result builders并非body专属。任何函数或闭包参数都可以被@ViewBuilder标注,届时其内部就可以合法地使用相同的DSL语法。2

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

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

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            content
        }
        .padding()
        .background(.regularMaterial)
        .clipShape(RoundedRectangle(cornerRadius: 12))
    }
}

// Usage: callers get the result-builder DSL inside the closure.
Card {
    Text("Title")
    Text("Subtitle")
    Image(systemName: "star")
}

这种模式正是SwiftUI自身的VStackHStackZStackListFormSectionGroupNavigationStack接受多个子视图的方式。其中每一种类型都接收一个@ViewBuilder content: () -> Content参数。识别出这种模式后,您就可以编写自己的容器视图,享有与框架相同的工效学,无需任何特殊的编译器支持。

之所以要写init(@ViewBuilder content:)而不是仅仅init(content:),是因为参数上的这个属性才能在调用方传入的闭包body内部激活result-builder转换。如果没有该属性,Card { Text("A"); Text("B") }就是编译错误,因为闭包包含两条语句而没有@ViewBuilder来转换它们。

状态、绑定与属性包装器层

以上内容讨论的都是视图树的形态。SwiftUI的另一半是状态,这一半建立在Swift属性包装器之上。7

与视图编写最相关的属性包装器:

@State在单个视图内部拥有一份值类型状态。读取该属性即读取底层存储;对其赋值则触发视图重新渲染。这种包装器适合简单、视图局部的状态(切换的开/关、文本字段的草稿字符串)。

@Binding是对另一个视图状态的双向引用。需要读写父视图状态的子视图会接收一个Binding<T>参数。父视图通过$state(在@State上的美元符号投影)来构造该绑定。

@Observable(iOS 17+)是一个宏,用于替代旧的ObservableObject遵循模式。应用于类的该宏会生成Observation框架的跟踪机制,从而使该类的属性在body内部被读取、稍后被修改时,触发视图重新渲染。视图侧对@Observable类的所有权从@StateObject转向了普通的@State;需要双向句柄的下游视图则使用@Bindable代替@ObservedObject

@Environment从环境链中读取依赖注入的值。SwiftUI提供了内置的环境键(locale、配色方案、关闭操作);应用程序可以为特定领域的依赖注入添加自定义键。

属性包装器层正是让视图body在状态变化时重新执行的机制。SwiftUI通过两种不同的机制跟踪body内部的读取:AttributeGraph(Apple私有的依赖图,支撑@State@Binding@Environment)用于较旧的属性包装器路径,以及标准库的Observation框架(withObservationTracking,在iOS 17+公开)用于@Observable类型。8 当被跟踪的属性发生变化时,相应的body会被重新执行,差异比对机制会计算出最小的视图树变化。

这两层(视图树层和状态层)是松散耦合的。视图树是值类型,重新计算速度很快。状态层是引用类型(对于@Observable)或带存储指针的值类型(对于@State),并跟踪读取。两者结合起来产生了框架”将屏幕上应有的内容描述为状态的函数,框架自行计算差异”的模型。

现在您能在错误信息中识别出什么

在底层基质可见的情况下阅读SwiftUI编译器错误:

“Function declares an opaque return type, but the return statements in its body do not have matching underlying types.”some View函数中,两个return语句返回不同的具体类型。修复方法:使用@ViewBuilder让result builder把两者都包装在_ConditionalContent中,或将两个return都包装在AnyView中。

“The compiler is unable to type-check this expression in reasonable time.” 带有大量修饰符的长body链耗尽了类型检查器。修复方法:将body拆分为更小的计算属性或子视图;每一段返回some View的代码都简化了类型推断的工作量。

“Cannot convert value of type ‘TupleView<…>’ to expected type ‘some View’.” 期望单一视图的函数收到了一个未带@ViewBuilder的多语句body结果。修复方法:在接收多语句内容的闭包参数上加@ViewBuilder

“Generic parameter ‘Content’ could not be inferred.” 一个自定义容器接收@ViewBuilder content: () -> Content,而调用方提供了空闭包。修复方法:result builders需要至少一个表达式来推断Content;如果调用方显式提供,空闭包可以回退到EmptyView()

错误信息之所以不友好,是因为底层基质不可见。在底层基质可见的情况下阅读它们,大多数错误就会变成”啊,result builder无法转换这个”或”啊,我需要分支或AnyView“。

何时跳出底层基质

底层基质有几种处理得不够干净的模式:

可变的具体类型。 一个根据分支返回不同View类型、且无法包装在result-builder分支中的函数,就需要AnyView。接受其代价(失去差异比对、没有动画),并在调用处加以注明。

跨平台条件视图。 编译时的#if os(iOS)@ViewBuilder body内部可以工作,但会限制result builder的分支数量;多OS的条件body有时会触及”表达式过于复杂”的限制。修复方法是将每个平台的子视图提取到独立的函数中,每个函数返回some View

命令式视图构造。 框架期望视图是表达式,而不是先构造再修改的对象。UIKit风格的”创建label、设置text、加入subview”无法直接迁移;其在SwiftUI中的等价做法是从body中返回值类型的Text("...")。需要命令式构造的模式通常意味着这部分工作应该放在UIViewRepresentable桥接到UIKit的代码中。

该模式对于在iOS 26+上交付的应用意味着什么

三个要点。

  1. SwiftUI是Swift,不是魔法。 Result builders、不透明返回类型和属性包装器都在Swift语言参考中有所记载。把框架当作Swift代码而非特殊DSL来阅读,会让那些令人意外的部分变得可预测。

  2. some ViewAnyView解决不同的问题。 不透明返回类型是默认选项;类型擦除是逃生出口。求助于AnyView应当是罕见情况;求助于some View配合result-builder分支应当是常见做法。

  3. Result builders就是整个DSL。 在任何函数或参数被@ViewBuilder标注的地方,都可以使用无逗号、语句式的语法。编写您自己的容器视图,享有与VStack相同的工效学,只需一个属性和一个闭包参数。

请将本文与该系列的已交付代码文章配套阅读:跨平台SwiftUI交付(Return基于一个共享的SwiftUI核心运行在五个平台上);Liquid Glass视觉层;iOS上的Live Activities状态机;Apple Watch上的watchOS运行时契约。汇总页面位于Apple生态系统系列。如需了解更宽泛的iOS与AI agents结合背景,请参阅iOS Agent开发指南

FAQ

SwiftUI中的View协议是什么?

View协议只有一个要求:var body: some View { get }。每个SwiftUI视图都是遵循View的Swift值类型,带有一个返回另一个视图(对于TextColorImageEmptyView等基本视图则返回Never)的body计算属性。body被@ViewBuilder标注,使其可以使用SwiftUI无逗号的DSL语法。

some View是什么意思?

some View是一种不透明返回类型(Swift 5.1+)。编译器知道具体类型;调用方只能看到协议见证。不透明类型让视图body可以返回像VStack<TupleView<(Text, Image, Spacer)>>这样的复杂类型而无需写出全名,同时保留编译时优化。some View一个特定类型,即使该类型在调用处不可见。

我应该何时使用AnyView?

只有在@ViewBuilder result-builder分支(ifswitchfor)和参数化泛型都无法解决问题时才使用AnyView。当包装的具体类型在不同渲染之间发生变化时,现有的视图层级会被销毁并在原位创建新的;这正是动画重启和视图状态重置的时刻。重新包装相同的具体类型不会触发这种销毁,但无论如何,框架所偏好的基于静态类型的差异比对都不再可用。如果您发现自己经常求助于AnyView,需要改变的模式在更上游:优先使用参数化视图,或将条件下推到result-builder body中。

@ViewBuilder是什么?可以在哪里使用?

@ViewBuilder是一种result builder(Swift语言特性)。它通过插入编译器合成的buildBlockbuildEitherbuildOptional等调用,将含有多个表达式的闭包转换为单一返回值。每个SwiftUI视图的body默认都是@ViewBuilder。您可以将@ViewBuilder应用于任何函数或闭包参数,让调用方获得相同的DSL语法;VStackCardSection都使用相同的模式来接收多个子视图。

为什么我的视图body在我意料之外地重新渲染了?

只要body读取的任何状态属性发生变化,SwiftUI就会重新运行body。属性包装器(@State@Binding@Observable@Environment)会跟踪读取并在写入时触发重新渲染。意外的重新渲染通常源自父视图的状态变化、环境值变化,或@Observable对象的某个被读取属性被修改。框架的差异比对机制随后会计算出最小的视图树变化。

References


  1. Apple Developer, “View” and “Configuring views”. The View protocol, Body associated type, and the @ViewBuilder attribute on body

  2. Swift Evolution, “SE-0289: Result builders”. The language proposal that formalized result builders (introduced as _functionBuilder in 5.1, formalized as @resultBuilder in 5.4). Defines buildBlock, buildEither, buildOptional, buildArray, buildExpression, buildFinalResult, and friends. 

  3. Apple Developer, “ViewBuilder” and “ForEach”. The result-builder type SwiftUI uses for view bodies (variadic-generic buildBlock, buildEither, optional unwrapping). ViewBuilder does not expose buildArray, so ForEach is the iteration primitive for repeating a view over a collection. 

  4. Swift Evolution, “SE-0244: Opaque result types”. The some keyword for opaque return types, added in Swift 5.1. 

  5. Apple Developer, “AnyView”. Type-erased view wrapper, construction, and the diffing trade-off. 

  6. Apple Developer, “Group”, “EmptyView”, and “TupleView”. Implementation types that result builders synthesize. 

  7. Apple Developer, “State and Data Flow”. The property-wrapper layer: @State, @Binding, @Observable, @Environment. SwiftUI’s observation system and the iOS 17+ @Observable macro. 

  8. Apple Developer, “Observation” and “Migrating from the Observable Object protocol to the Observable macro”. The standard-library Observation framework, including withObservationTracking(_:onChange:), plus the iOS 17 migration path from ObservableObject to @Observable

相关文章

watchOS Runtime Is a Contract, Not a Background Task

watchOS does not have iOS's background. WKExtendedRuntimeSession is a contract you sign with the system, broken on wrist…

15 分钟阅读

RealityKit And The Spatial Mental Model

RealityKit is an entity-component-system, not SwiftUI in 3D. Anchors place entities in real space. Five ways the model d…

16 分钟阅读

The Cleanup Layer Is the Real AI Agent Market

Charlie Labs pivoted from building agents to cleaning up after them. The AI agent market is moving from generation to pr…

15 分钟阅读