SwiftUI ViewBuilder 可以寫的程式

撰寫 SwiftUI 程式時,我們時常看到類似以下 VStack & HStack 的程式,{ } 裡寫了多行產生 UI 元件的程式。

  • VStack 範例。
struct ContentView: View {
var body: some View {
VStack {
Text("對的時間錯的人")
Text("錯的時間錯的人")
Text("錯的時間對的人")
Text("對的時間對的人")
}
}
}
  • HStack 範例。
struct ContentView: View {
var body: some View {
HStack {
Text("對的時間錯的人")
Text("錯的時間錯的人")
Text("錯的時間對的人")
Text("對的時間對的人")
}
}
}

VStack 的相關定義如下,我們剛剛傳入的 { } 程式是它的第三個參數 content。看來我們應該要在 { } 裡回傳產生的 UI 元件,為何我們可以連寫四行產生 Text 元件的程式呢 ?

@frozen struct VStack<Content> where Content : View

init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)

關鍵來自參數 content 前的 @ViewBuilder

ViewBuilder 其實是個型別,背後用到 function builders 的語法。當 @ViewBuilder 加在剛剛的參數 content 時,Swift 會將我們傳入 closure { } 裡多行產生 UI 元件的程式組合成一個 view,因此完全符合 content 參數型別的要求,回傳一個遵從 View protocol 的元件。

如果沒有 ViewBuilder

如果沒有 ViewBuilder,直接在 { } 裡產生 UI 元件將沒有任何效果,讓我們看看以下 Button 的例子。

struct ContentView: View {
var body: some View {
Button {
Text("雷蒙·錢德勒")
Text("達許·漢密特")
} label: {
Text("Button")
}
}
}

Button 的 init 如下。

public init(action: @escaping () -> Void, @ViewBuilder label: () -> Label)

第一個參數 action 沒有 ViewBuilder,傳入的 closure 代表按鈕點選要做的事。我們在裡面產生兩個 Text 後將產生以下警告。它根本不會使用到我們產生的 Text,因此顯示 Result of ‘Text’ initializer is unused。

@ViewBuilder 參數可以寫的程式

有了 @ViewBuilder,我們可以方便地在參數 closure 的 { } 裡生成多個元件,但請注意它跟一般的 closure 不太一樣,不是什麼程式都可以寫。目前 ViewBuilder 裡可以寫的程式如下。

  • if else 的例子。
struct ContentView: View {
var number = Int.random(in: 1...100)

var body: some View {
VStack {
if number % 2 == 1 {
Text("錯的時間對的人")
} else {
Text("對的時間對的人")
}
}
}
}
  • if let、switch 和宣告變數常數。

ViewBuilder 裡不能寫的程式

  • 產生字串,而不是產生 view。
  • 呼叫 function。

單獨寫一行呼叫 function 的程式是不允許的。

值得注意的,若改成 let _ = print("是你嗎 ?") 卻是可以的,因為 ViewBuilder 裡可以宣告變數常數。

struct ContentView: View {
var body: some View {
VStack {
let _ = print("是你嗎 ?")
Text("對的時間錯的人")
Text("錯的時間錯的人")
Text("錯的時間對的人")
Text("對的時間對的人")
}
}
}
  • 使用 for 迴圈。

舊版 ViewBuilder 的元件數量限制

從 Xcode 15 開始,ViewBuilder 不再有元件數量的限制。

舊版則有上限 10 個 view 的限制,因此若我們產生 11 個 view,它將無福消受,出現紅色錯誤 Extra argument in call。( 舊版的錯誤訊息是 Ambiguous reference to member buildBlock()。 )

利用 Group 合併元件解決數量問題

看來這是很嚴重的問題呀,這樣我們的 VStack 豈不是只能放十個元件 ? 別擔心,SwiftUI 提供了將多個元件組合的 Group ,我們可利用它合併元件解決數量問題。

利用 ForEach 實現迴圈

ViewBuilder 裡沒有提供 for 相關的 function,因此我們不能偷懶用 for 產生多個元件。由於利用迴圈顯示多個元件的需求十分常見,尤其是搭配 array 時,因此 SwiftUI 提供了 ForEach 型別幫助我們,我們可以使用它將 array 的內容變成一個個 view,然後組合起來。

struct ContentView: View {
let songs = ["不懂", "江南", "害怕", "一千年以後", "簡簡單單", "原來", "西界", "小酒窩", "她說", "愛笑的眼睛", "不潮不用花錢"]

var body: some View {
VStack {
ForEach(songs, id: \.self) { name in
Text(name)
}
}
}
}

判斷 SwiftUI 的 function 型別參數是否是 ViewBuilder

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com