SwiftUI 小技巧:透過 PreferenceKey 簡單對齊視圖
假設你有一個包含三個 TextField
的簡單視圖,並且編寫了下列程式碼:
struct PreferenceTest: View { @State var value1 = "" @State var value2 = "" @State var value3 = "" var body: some View { Form { HStack { Text("First Item") TextField("Enter first item", text: $value1) } HStack { Text("Second Item") TextField("Enter second item", text: $value2) } HStack { Text("Final Item") TextField("Enter third item", text: $value3) } } } }
運行時,你希望會看到這樣的畫面:

但是,你記得自己閱讀過有關 Spacer()
的內容,因此嘗試將其添加到 Text
和 TextField
物件之間。
你猜怎麼了?甚麼都沒有改變。
所以你開始在網路上尋找解決方案,但甚麼都沒有用。然後,一個朋友問你有否看過 WWDC 19 有關 SwiftUI 的影片。你還沒有看過,所以花了一個小時看那兩個男人讚美 SwiftUI 的優點。你覺得很興奮,但卻錯過了大約 52 分鐘重要的幾秒鐘,Preferences 在那短短幾秒鐘中被提及。
Preferences 是很聰明的工具,尤其是 PreferenceKey
協定,就像這樣:
public protocol PreferenceKey { associatedtype Value static var defaultValue: Self.Value { get } static func reduce(value: inout Self.Value, nextValue: () -> Self.Value) }
在本文的下半部,我將展示如何使用它來達到這樣的結果:

根據 GitHub 文件的定義,PreferenceKey
是視圖生成的佈局偏好,有多個子視圖的視圖,會自動將設定值合併為父類視圖可見的佈局條件。
以下的 ColumnWidthPreferenceKey
是我遵循 PreferenceKey
的 struct 實作:
struct ColumnWidthPreference: Equatable { let width: CGFloat } struct ColumnWidthPreferenceKey: PreferenceKey { typealias Value = [ColumnWidthPreference] static var defaultValue: [ColumnWidthPreference] = [] static func reduce(value: inout [ColumnWidthPreference], nextValue: () -> [ColumnWidthPreference]) { value.append(contentsOf: nextValue()) } }
從 Text
視圖收集到的 Value
是它們的寬度,我們通過 reduce
函式將它們轉成 ColumnWidthPreference
的陣列。
那這是怎樣發生的呢?
以下是完整的範例程式碼:
struct PreferenceTest: View { @State var value1 = "" @State var value2 = "" @State var value3 = "" @State var width: CGFloat? = nil var body: some View { Form { HStack { Text("First Item") .frame(width: width, alignment: .leading) .background(ColumnWidthEqualiserView()) TextField("Enter first item", text: $value1) .textFieldStyle(RoundedBorderTextFieldStyle()) } HStack { Text("Second Item") .frame(width: width, alignment: .leading) .background(ColumnWidthEqualiserView()) TextField("Enter second item", text: $value2) .textFieldStyle(RoundedBorderTextFieldStyle()) } HStack { Text("Final Item") .frame(width: width, alignment: .leading) .background(ColumnWidthEqualiserView()) TextField("Enter third item", text: $value3) .textFieldStyle(RoundedBorderTextFieldStyle()) } }.modifier(ColumnWidth(width:$width)) } }
如你所見,它是一個由三個 HStacks
組成的 Form
,每個 HStack 包含一個 Text
視圖和一個 TextField
視圖,而它們每個都綁定到一個初始值。在本文中,重點設定包括:
Text
修飾器 (modifier):frame
和background
。Form
修飾器:modifier
。
background
的參數是 columnWidthEqualiserView
,定義為:
struct ColumnWidthEqualiserView: View { var body: some View { GeometryReader { geometry in Rectangle() .fill(Color.clear) .preference( key: ColumnWidthPreferenceKey.self, value: [ColumnWidthPreference(width: geometry.frame(in: CoordinateSpace.global).width)] ) } } }
在這裡,我們宣告了一個 GeometryReader
,它被定義為一個容器視圖,內容定義為其自身大小和坐標空間 (coordinate space)。
它是通過 GeometryProxy
來實作的,GeometryProxy
可以讓我們存取容器視圖的大小和坐標空間。
在範例程式碼中,容器視圖是 Text
視圖。在 GeometryReader
之後是一個閉包,閉包內的參數就是 GeometryProxy
。閉包的工作,就是在容器中創建一個 Rectangle
,並透過 fill
修飾器修改,以 clear
來填滿視圖顏色,並使用 preference
替 ColumnWidthPreferenceKey
建置一組 key/value。如此一來,它就可以為 Text
視圖構建寬度陣列了。
Form
修飾器的參數為 ColumnWidth
,定義為:
struct ColumnWidth: ViewModifier { @Binding var width: CGFloat? func body(content: Content) -> some View { content .onPreferenceChange(ColumnWidthPreferenceKey.self) { preferences in for p in preferences { let oldWidth = self.width ?? CGFloat.zero if p.width > oldWidth { self.width = p.width + 20 } } } } }
以上程式碼是對 ColumnWidthPreferenceKey
更改作出的回應。
然後,它遍歷所有子視圖的 Preference key/value,尋找最寬的 Text
視圖 (+20),並將傳遞給它的參數保存為 width
。接著,我們會使用新的寬度重新構建視圖,這是 frame
設置的地方。
這有點複雜,但我們已經成功了。
聯絡方式:電郵:[email protected]
FB: https://www.facebook.com/yishen.chen.54
Twitter: https://twitter.com/YeEeEsS