SwiftUI 小技巧:透過 PreferenceKey 簡單對齊視圖


本篇原文(標題:Using the PreferenceKey Protocol to Align Views in SwiftUI )刊登於作者 Medium,由 Keith Lander 所著,並授權翻譯及轉載。

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

運行時,你希望會看到這樣的畫面:

demo-1

但是,你記得自己閱讀過有關 Spacer() 的內容,因此嘗試將其添加到 TextTextField 物件之間。

你猜怎麼了?甚麼都沒有改變。

所以你開始在網路上尋找解決方案,但甚麼都沒有用。然後,一個朋友問你有否看過 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)
}

在本文的下半部,我將展示如何使用它來達到這樣的結果:

swiftui-preferencekey

根據 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):framebackground
  • 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 來填滿視圖顏色,並使用 preferenceColumnWidthPreferenceKey 建置一組 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 設置的地方。

這有點複雜,但我們已經成功了。

本篇原文(標題:Using the PreferenceKey Protocol to Align Views in SwiftUI)刊登於作者 Medium,由 Keith Lander 所著,並授權翻譯及轉載。

作者簡介:Keith Lander,擁有 52 年軟體開發經驗,是 Writing Shed 開發者,Writing Shed 這個 iOS & Mac App 是特別為寫作者而設的。

譯者簡介:陳奕先-過去為平面財經記者,專跑產業新聞,2015 年起跨進軟體開發世界,希望在不同領域中培養新的視野,於新創學校 ALPHA Camp 畢業後,積極投入 iOS 程式開發,目前任職於國內電商公司。

聯絡方式:電郵:[email protected]
FB: https://www.facebook.com/yishen.chen.54
Twitter: https://twitter.com/YeEeEsS


此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。

blog comments powered by Disqus
Shares
Share This