SwiftUI 的構成元素
SwiftUI 建構於三項 Swift 語言特性之上:result builders(結果建構器)、opaque return types(不透明回傳型別)以及值型別的 view 樹。一旦看清這些底層基礎,那些讓開發者感到困惑的 SwiftUI 部分(AnyView、Group、ViewBuilder、@ViewBuilder 參數,以及令人頭痛的 some View vs any View 錯誤)便不再神祕。
SwiftUI view 是符合單一協定、僅有單一要求的值型別。框架的其餘部分都建構在 SwiftUI 之外的 Swift 語言特性上:result builders、opaque types、帶約束的泛型、property wrappers。如果您理解這些語言特性,整個框架讀起來就像一般的 Swift API。如果不理解,框架讀起來就像偶爾會反咬一口的魔法。
本文將逐步走過這些底層基礎。這裡沒有 LiveActivityManager,也沒有 Get Bananas 的截圖。重點是框架本身,而非某個專案;一旦框架變得清晰可讀,整個系列中每一篇實際出貨程式碼的文章也會讀得更通順。
TL;DR
- SwiftUI view 是符合
View協定的 Swift 值型別。該協定只有一項要求:var body: some View { get }。其餘一切都建構在 Swift 語言特性之上。 @ViewBuilder是一個 result builder。每個View的 body 都是一個 result builder。Result builders 透過編譯器合成的呼叫,將以逗號分隔的表達式轉化為單一回傳值。some View是不透明回傳型別。編譯器知道具體型別;呼叫端不知道。這個不透明型別正是讓 view body 在編譯與執行時都能保持高效的關鍵;AnyView則是不透明性無法運作時的型別抹除逃生口。Group、EmptyView、TupleView、_ConditionalContent是 result builders 合成的實作型別。它們有公開文件,但很少由人手動撰寫。
一切的起點:那個協定
View 協定只有一項要求:1
public protocol View {
associatedtype Body : View
@ViewBuilder var body: Self.Body { get }
}
這個協定中有兩個部分對於理解 SwiftUI 的其餘部分至關重要。
關聯型別 Body : View。 view 的 body 本身也是一個 view。這種遞迴正是讓框架可組合的原因。每個 View 的 body 都會回傳另一個 View,依此類推,直到鏈條終結於框架的某個 primitive view(基礎 view,例如 Text、Color、Image、EmptyView),其 Body 為 Never。Primitive views 是樹的葉節點;您撰寫的 views 則是分支。
body 上的 @ViewBuilder 屬性。 每個 body 都是一個 result-builder closure。Result builders 是 SE-0289 中規範的 Swift 語言特性(在 Swift 5.4 中以 @resultBuilder 正式化),讓包含一系列表達式的 closure 能由編譯器透過合成的方法呼叫,轉換為單一回傳值。2 這項轉換正是讓 SwiftUI body 內部那種無逗號、語句式的語法得以運作的原因。
這個協定的形狀有兩點不尋常之處。
第一,要求是計算屬性,而非方法。當 SwiftUI 判定 view 的 state 已變更時,會在每次渲染傳遞中重新計算 view 的 body。框架將 body 視為呼叫成本低廉的屬性;在 body 中執行長時間計算是反模式,因為它會在每次渲染時執行。
第二,Self.Body 是關聯的,而非抹除的。view 的具體 body 型別在編譯期就是其簽章的一部分。Text("Hello") 的 body 型別是 Never;自訂 view 的 body 型別則是 @ViewBuilder 為該 body 合成出來的型別。這種關聯型別設計,讓編譯器無需執行期型別檢查就能最佳化 view 樹。同時也是當自訂 view 回傳條件式內容時,會出現 some View 要求的原因。
Result Builders:無逗號的 DSL
Result builder 是 Swift 語言特性,透過插入編譯器合成的方法呼叫,將 closure 轉換為單一回傳值。@ViewBuilder 就是一個 result builder。每個 SwiftUI view 的 body 都是它的 closure。2
考慮以下這個 view:
struct ExampleView: View {
var body: some View {
Text("Title")
Text("Subtitle")
Image(systemName: "star")
}
}
這個 body 有三個語句,沒有任何分隔符號。在一般的 Swift 中,這是編譯錯誤:closure 只能回傳一個值。Result builders 在編譯前重寫 closure。經過 @ViewBuilder 展開後,編譯器實際看到的程式碼大致如下:
struct ExampleView: View {
var body: some View {
ViewBuilder.buildBlock(
Text("Title"),
Text("Subtitle"),
Image(systemName: "star")
)
}
}
ViewBuilder.buildBlock(_:_:_:) 是一個靜態方法,接收三個 views 並回傳 TupleView<(Text, Text, Image)>。Body 回傳的就是這單一的 tuple-view 值。較舊的 SwiftUI 為 1、2、3、…直到 10 個子元素提供固定的 buildBlock 多載;目前的 SwiftUI 採用 Swift 的 variadic-generics 支援(buildBlock<each Content>),因此具有 11 個或更多兄弟 view 的 body 不再是特例。
控制流程也以同樣的模式處理。包含 if 語句的 view 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 let、switch、optional 解開以及其他幾種建構,都由 result builder 的各種 buildXxx 靜態方法處理。3 重複內容是該語言特性透過 buildArray 支援、但 @ViewBuilder 不支援的一個顯著案例:在 body 中直接寫 for 迴圈會失敗並出現錯誤訊息 「closure containing control flow statement cannot be used with result builder ‘ViewBuilder’.」。SwiftUI 風格的解法是 ForEach,它接收一個 RandomAccessCollection 與內容 closure,並將迭代合成為單一的值型別 view。這個 DSL 並非客製化的東西;它就是 Swift 的 result builders,為 view 量身設定。
some View 與不透明性問題
自訂 view 的 body 通常回傳 some View。這個關鍵字是 opaque return type(不透明回傳型別),於 Swift 5.1 加入。4
some View 表示:「我回傳一個符合 View 的特定型別,但我不告訴您是哪一個。」編譯器內部追蹤具體型別以進行最佳化;您的 view 的呼叫端只看見協定的 witness。這個模式讓 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 View 與 any View 不同。 some View 是不透明的(一個特定型別,被隱藏);any View 是 existential(一個能容納任何符合型別的盒子,帶有執行期開銷)。獨立函式或屬性 可以 合法地回傳 any View。然而 View 協定的 body 不行:協定要求 associatedtype Body: View,而 any View 本身並不符合 View,因此 var body: any View 無法滿足協定,編譯器會建議改用 some View。實務原則:view body 使用 some View;對於需要執行期變動 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 是型別抹除的 view 包裹器。其建構方式為 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 的代價是:底層型別不再是 view 靜態身分的一部分。Apple 文件直接描述了這個後果:當包裹在 AnyView 內的型別在多次渲染之間改變時,現有的 view 階層會被銷毀,並在原處建立新的階層,這意味著遺失 state、動畫重新開始、身分喪失。重新包裹相同的具體型別不會觸發這種銷毀,但無論哪種情況,框架偏好的靜態型別驅動的 diffing 都不再可用。
正確的原則是:對於條件式 views(if、switch、for),優先採用 @ViewBuilder 的 result-builder 分支;對於變動型別,優先採用參數化 views;只有在兩者都無法解決時,才動用 AnyView。如果 func viewForKind 回傳 AnyView,通常代表您應該讓 viewForKind 回傳 some View,並將 switch 放進 result-builder closure 中。
Group、EmptyView、TupleView:實作型別
Result builder 會合成出特定的具體 view 型別。其中三個值得辨識:6
Group 是透明的容器。它最多接受十個 views 作為內容,並將它們作為兄弟元素呈現給父層 layout。容器本身不增加任何視覺結構;內容的渲染與單獨呈現時完全相同。使用情境是:將多個 views 包進預期單一 view 的上下文中(如 .if modifier、條件式 return、產生「一個 view」的函式)。Group { Text("A"); Text("B") } 是包含兩個元素的單一 view;它是 result builders 隱含執行的明確形式。
EmptyView 是不渲染任何內容的 view。當 if 沒有 else 時,result builder 會將其作為條件 false 分支。從您自己的程式碼回傳 EmptyView(),是在不改變函式回傳型別的情況下選擇不渲染的方式。
TupleView 是當 body 包含多個兄弟 views 時,result builders 產生的具體型別。本文開頭那個回傳三個兄弟 views 的表達式,實際回傳的是 TupleView<(Text, Text, Image)>。您幾乎不會直接撰寫 TupleView;通常只在錯誤訊息中讀到它。
_ConditionalContent(前置底線)是處理 if/else 分支的型別。這個型別出現在 ViewBuilder 的公開介面中,但底線開頭的命名暗示「不要隨意撰寫此型別」;讓 result builder 從 if/else 合成它,而非手動建構。
在您自己的函式上使用 @ViewBuilder
Result builders 並非僅供 body 使用。任何函式或 closure 參數都可標註 @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 自家的 VStack、HStack、ZStack、List、Form、Section、Group 與 NavigationStack 接受多個子元素的方式。這些型別每一個都接收 @ViewBuilder content: () -> Content 參數。認識此模式意味著:您可以撰寫具備與框架相同人體工學的自訂容器 view,無需任何特殊的編譯器支援。
之所以寫 init(@ViewBuilder content:) 而非單純 init(content:),是因為標註在參數上的這個屬性,正是啟動呼叫端傳入的 closure body 內 result-builder 轉換的關鍵。如果沒有此屬性,Card { Text("A"); Text("B") } 會是編譯錯誤,因為 closure 有兩個語句,卻沒有 @ViewBuilder 來轉換它們。
State、Bindings 與 Property Wrapper 層
以上所有內容都關乎 view 樹的 形狀。SwiftUI 的另一半是 state,而那一半建構在 Swift property wrappers 之上。7
對 view 撰寫最相關的 property wrappers 包括:
@State 在單一 view 內擁有一份值型別 state。讀取屬性即讀取底層儲存;對其賦值會觸發 view 重新渲染。此 wrapper 適用於簡單、view 本地的 state(例如切換按鈕的開關、文字欄位的草稿字串)。
@Binding 是對另一個 view state 的雙向參考。需要讀寫父層 state 的子 view 接收一個 Binding<T> 參數。父層透過 $state(@State 上的錢字符投影)建構這個 binding。
@Observable(iOS 17+)是一個 macro,取代了較舊的 ObservableObject 一致性模式。將此 macro 套用於 class,會產生 Observation 框架的追蹤功能,使 class 的屬性在 body 內被讀取後再被變更時,能夠觸發 view 重新渲染。對於 @Observable class 的 view 端擁有權,從 @StateObject 改為單純的 @State;需要雙向控制權的下游 views 則使用 @Bindable,而非 @ObservedObject。
@Environment 從環境鏈中讀取依賴注入的值。SwiftUI 提供內建的 environment keys(locale、color scheme、dismiss action);應用程式可加入自訂 keys,用於領域特定的依賴注入。
Property-wrapper 層正是讓 view 的 body 能在 state 變更時重新執行的關鍵。SwiftUI 透過兩種不同機制追蹤 body 內部的讀取:AttributeGraph(Apple 的私有依賴圖,支援 @State、@Binding、@Environment)負責較舊的 property-wrapper 路徑;標準函式庫的 Observation 框架(withObservationTracking,自 iOS 17+ 公開)則負責 @Observable 型別。8 當被追蹤的屬性發生變動時,對應的 bodies 會重新執行,diffing 機制會計算最小的 view 樹變更。
這兩半(view 樹層與 state 層)為鬆耦合關係。View 樹是值型別,重新計算成本低。State 層則是參考型別(針對 @Observable)或附帶儲存指標的值型別(針對 @State),並追蹤讀取。兩者結合,產生了框架那種「將畫面應呈現的內容描述為 state 的函式,框架自行算出 diff」的模型。
您現在能在錯誤訊息中辨識的內容
帶著對底層基礎的認識去閱讀 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.」 帶有許多 modifier 的長 body 鏈耗盡了型別檢查器。修正方式:將 body 拆分為較小的計算屬性或子 views;每個回傳 some View 的片段都能簡化推論工作。
「Cannot convert value of type ‘TupleView<…>’ to expected type ‘some View’.」 一個預期單一 view 的函式收到了缺少 @ViewBuilder 的多語句 body 結果。修正方式:在接受多語句內容的 closure 參數上加上 @ViewBuilder。
「Generic parameter ‘Content’ could not be inferred.」 一個自訂容器接收 @ViewBuilder content: () -> Content,呼叫端傳入空 closure。修正方式:result builders 至少需要一個表達式才能推論 Content;空 closure 僅在呼叫端明確提供 EmptyView() 時才能 fallback。
這些錯誤訊息之所以不友善,正因為底層基礎是不可見的。帶著對底層基礎的認識去讀,多數訊息便轉化為「啊,result builder 無法轉換這個」或「啊,我需要分支或 AnyView」。
何時應跳脫底層基礎之外
少數模式是底層基礎無法乾淨處理的:
多型具體型別。 當函式為每個分支回傳不同的 View 型別,且您無法將其包裹在 result-builder 分支中時,需要 AnyView。接受其代價(喪失 diffing、無動畫),並在呼叫端記錄清楚。
跨平台條件式 views。 編譯期的 #if os(iOS) 在 @ViewBuilder body 內可運作,但會限制 result builder 的分支數量;多 OS 條件式 bodies 有時會撞上「expression too complex」上限。修正方式是將每個平台的子 views 抽取到各自的函式中,每個函式回傳 some View。
命令式 view 建構。 框架預期 views 是表達式,而非建構後再變動的物件。UIKit 風格的「建立 label、設定 text、加入 subview」並不適用;SwiftUI 的對應做法是從 body 回傳值型別的 Text("...")。需要命令式建構的模式通常意味著該工作屬於透過 UIViewRepresentable 橋接到 UIKit 的範疇。
此模式對於在 iOS 26+ 上出貨的 App 意味著什麼
三個重點。
-
SwiftUI 是 Swift,不是魔法。 Result builders、不透明回傳型別與 property wrappers 全都記載於 Swift 語言參考中。將框架當作 Swift 程式碼閱讀,而非當作特殊 DSL,能讓那些令人意外的部分變得可預期。
-
some View與AnyView解決不同的問題。 不透明回傳型別是預設選項;型別抹除是逃生口。動用AnyView應為罕見情境;採用some View加上 result-builder 分支應為常態。 -
Result builders 就是整個 DSL。 任何標註
@ViewBuilder的函式或參數,都能使用這套無逗號、語句式的語法。撰寫具備與VStack相同人體工學的自訂容器 view,只需要一個屬性與一個 closure 參數。
請將本文與本系列的實際出貨程式碼系列搭配閱讀:跨平台的 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 都是符合 View 的 Swift 值型別,具有一個 body 計算屬性,回傳另一個 view(對於 Text、Color、Image、EmptyView 這類 primitive views,則回傳 Never)。Body 標註了 @ViewBuilder,因此可以使用 SwiftUI 那種無逗號的 DSL 語法。
some View 是什麼意思?
some View 是不透明回傳型別(Swift 5.1+)。編譯器知道具體型別;呼叫端只看見協定的 witness。不透明型別讓 view body 能回傳如 VStack<TupleView<(Text, Image, Spacer)>> 這類複雜型別,而無需明確寫出,同時保留編譯期最佳化。some View 是 一個特定型別,儘管該型別在呼叫端不可見。
我何時應使用 AnyView?
只有當 @ViewBuilder 的 result-builder 分支(if、switch、for)以及參數化泛型都無法解決問題時,才使用 AnyView。當被包裹的具體型別在多次渲染之間改變時,現有的 view 階層會被銷毀並在原處建立新的階層;那一刻動畫會重新開始、view state 會被重設。重新包裹相同的具體型別不會觸發這種銷毀,但無論哪種情況,框架偏好的靜態型別驅動的 diffing 都不再可用。如果您發現自己經常動用 AnyView,需要改變的模式其實在更上游:優先使用參數化 views,或將條件式推進 result-builder body 中。
@ViewBuilder 是什麼?我可以在哪裡使用它?
@ViewBuilder 是一個 result builder(Swift 語言特性)。它透過插入編譯器合成的 buildBlock、buildEither、buildOptional 等呼叫,將包含多個表達式的 closure 轉換為單一回傳值。每個 SwiftUI view 的 body 預設就是 @ViewBuilder。您可以將 @ViewBuilder 套用到任何函式或 closure 參數,讓呼叫端獲得相同的 DSL 語法;VStack、Card 與 Section 都使用相同模式來接受多個子元素。
為什麼我的 view body 會在我沒預期的情況下重新渲染?
每當 body 讀取的任何 state 屬性發生變動時,SwiftUI 就會重新執行 body。Property wrappers(@State、@Binding、@Observable、@Environment)追蹤讀取,並在寫入時觸發重新渲染。意料之外的重新渲染通常可追溯到父 view 的 state 變更、environment 值變更,或某個 @Observable 物件被讀取的屬性遭到修改。框架的 diffing 隨後計算最小的樹變更。
參考資料
-
Apple Developer,「View」 與 「Configuring views」。
View協定、Body關聯型別,以及body上的@ViewBuilder屬性。 ↩ -
Swift Evolution,「SE-0289: Result builders」。將 result builders 正式化的語言提案(在 5.1 中以
_functionBuilder引入,在 5.4 中以@resultBuilder正式化)。定義了buildBlock、buildEither、buildOptional、buildArray、buildExpression、buildFinalResult等。 ↩↩↩ -
Apple Developer,「ViewBuilder」 與 「ForEach」。SwiftUI 用於 view body 的 result-builder 型別(variadic-generic 的
buildBlock、buildEither、optional 解開)。ViewBuilder並未公開buildArray,因此ForEach是用於在集合上重複 view 的迭代基本單元。 ↩ -
Swift Evolution,「SE-0244: Opaque result types」。用於不透明回傳型別的
some關鍵字,於 Swift 5.1 加入。 ↩ -
Apple Developer,「AnyView」。型別抹除的 view 包裹器、其建構方式,以及 diffing 上的取捨。 ↩
-
Apple Developer,「Group」、「EmptyView」 與 「TupleView」。Result builders 合成的實作型別。 ↩
-
Apple Developer,「State and Data Flow」。Property-wrapper 層:
@State、@Binding、@Observable、@Environment。SwiftUI 的觀察系統與 iOS 17+ 的@Observablemacro。 ↩ -
Apple Developer,「Observation」 與 「Migrating from the Observable Object protocol to the Observable macro」。標準函式庫的 Observation 框架,包含
withObservationTracking(_:onChange:),以及從ObservableObject遷移至@Observable的 iOS 17 路徑。 ↩