假設你有一個包含三個 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