[SwiftUILab]探究View樹part2AnchorPreferences(錨定偏好)

NO IMAGE

文章源地址:swiftui-lab.com/communicati…

作者: Javier

翻譯: Liaoworking

第一部分的文章中,我們介紹了偏好(preferences)的使用,它可以很有用的把信息向上傳遞(從子級視圖傳到祖級視圖)。通過定義PreferenceKey的關聯類型,可以獲取到所有想要的數據。

在第二部分,我們將介紹 錨定偏好 (Anchor Preferences 寫的時候國內還沒有對應的名詞翻譯,這裡憑個人理解硬翻),在寫這篇文章的時候還沒有找到任何相關文檔、博客或者文章來介紹如何使用這些很難理解的工具類。那就讓我來介紹一下吧。

錨定偏好看字面意思並不好理解。但只要我們掌握了,就很難忘卻了。還是通過第一部分裡面的例子來講。這裡不會用到之前的空間座標系來解決。我們將用其他方法來替換.onPreferenceChange()

這裡在簡單提及例子裡所做的事情:點擊不同的月份的時候邊框會從一個月份移動到另外一個月份上面,並帶有動畫效果。

[SwiftUILab]探究View樹part2AnchorPreferences(錨定偏好)

錨定偏好

首先迎來的是Anchor< T >, 這是存放泛型T的不透明的類型。 這裡的T可以是CGRect或者是CGPoint。我們一般使用Anchor來獲得視圖的大小,用Anchor來獲取例top, topLeading, topTrailing, center, trailing, bottom, bottomLeading, bottomTrailing, leading屬性。

因為這是不透明類型,所以我們不能單獨使用它。還記得之前的文章GeometryReader to the Rescue文章中GeometryProxy的通過下標getter方法麼。現在你應該知道了,當使用Anchor的值作為 geometry proxy 的索引時,你就可以獲得CGRect和CGPoint的值。此外,你還可以獲取它們在GeometryReader視圖中的空間座標。

我們先通過修改PreferenceKey處理的數據開始吧,在這個例子中我們把CGRect替換成了Anchor

struct MyTextPreferenceData {
let viewIdx: Int
let bounds: Anchor<CGRect>
}

我們的PreferenceKey 保持不變

struct MyTextPreferenceKey: PreferenceKey {
typealias Value = [MyTextPreferenceData]
static var defaultValue: [MyTextPreferenceData] = []
static func reduce(value: inout [MyTextPreferenceData], nextValue: () -> [MyTextPreferenceData]) {
value.append(contentsOf: nextValue())
}
}

MonthView的代碼就變的更簡潔了,把MonthView的.preference()替換成.anchorPreference()。和其他方法不同,這裡我們可以指定一個值(例子裡面指定的是.bounds)。 那麼我們transform這閉包中的Anchor就是修改視圖的bounds。 和處理普通的偏好相似,我們用{$0}來創建MyTextPreferenceData值。這樣的話我們就不需要在.background() 中使用GeometryReader來獲取text View的bounds了。

代碼如下:

struct MonthView: View {
@Binding var activeMonth: Int
let label: String
let idx: Int
var body: some View {
Text(label)
.padding(10)
.anchorPreference(key: MyTextPreferenceKey.self, value: .bounds, transform: { [MyTextPreferenceData(viewIdx: self.idx, bounds: $0)] })
.onTapGesture { self.activeMonth = self.idx }
}
}

最後,更新我們的ContentView,這裡會有一些變化。對初學者來說,我們不再使用.onPreferenceChange(),而是使用.backgroundPreferenceValue()。這是一個類似於.background()的修改器。
但它有一個很大的好處就是:
我們可以獲取到整個視圖樹的偏好(preference)數組。
這樣的話,我們也可以通過獲取到所有的月份視圖的Bounds信息來計算出邊框應該繪製在哪裡。

#warning()

在Xcode 11 beta5中,蘋果悄悄的 用 Equatable 移除了 Anchor 的一致性。 如果你想要使用 .onPreferenceChange() , 你大概能想象到,需要你的preference key的值符合 Equatable 協議。幸運的是例子中沒有使用到 .onPreferenceChange() , 自從Anchor的一致性被棄用之後我就一直希望在 GM版本發佈之前恢復。 我提交了一個錯誤報告(FB6912036), 希望你也能這樣。

仍然還有一個地方需要用到GeometryReader,通過它我們可以不用關心空間座標,也讓Anchor的值變的有用。

struct ContentView : View {
@State private var activeIdx: Int = 0
var body: some View {
VStack {
Spacer()
HStack {
MonthView(activeMonth: $activeIdx, label: "January", idx: 0)
MonthView(activeMonth: $activeIdx, label: "February", idx: 1)
MonthView(activeMonth: $activeIdx, label: "March", idx: 2)
MonthView(activeMonth: $activeIdx, label: "April", idx: 3)
}
Spacer()
HStack {
MonthView(activeMonth: $activeIdx, label: "May", idx: 4)
MonthView(activeMonth: $activeIdx, label: "June", idx: 5)
MonthView(activeMonth: $activeIdx, label: "July", idx: 6)
MonthView(activeMonth: $activeIdx, label: "August", idx: 7)
}
Spacer()
HStack {
MonthView(activeMonth: $activeIdx, label: "September", idx: 8)
MonthView(activeMonth: $activeIdx, label: "October", idx: 9)
MonthView(activeMonth: $activeIdx, label: "November", idx: 10)
MonthView(activeMonth: $activeIdx, label: "December", idx: 11)
}
Spacer()
}.backgroundPreferenceValue(MyTextPreferenceKey.self) { preferences in
return GeometryReader { geometry in
ZStack {
self.createBorder(geometry, preferences)
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
}
func createBorder(_ geometry: GeometryProxy, _ preferences: [MyTextPreferenceData]) -> some View {
let p = preferences.first(where: { $0.viewIdx == self.activeIdx })
let bounds = p != nil ? geometry[p!.bounds] : .zero
return RoundedRectangle(cornerRadius: 15)
.stroke(lineWidth: 3.0)
.foregroundColor(Color.green)
.frame(width: bounds.size.width, height: bounds.size.height)
.fixedSize()
.offset(x: bounds.minX, y: bounds.minY)
.animation(.easeInOut(duration: 1.0))
}
}

.backgroundPreferenceValue() 相對應的是.overlayPreferenceValue(), 它們的作用相同,只不過一個是繪製背景,一個是繪製前景。

單個 PreferenceKey 和 多個錨定偏好

我們知道Anchor 的值不止有bounds,還有topLeading, center, bottom等值。可能有的情況下我們需要的不止一個Anchor 的值,然而,調用它並不像調用.anchorPreference() 一樣容易。下面我們舉例繼續說明。
我們將使用兩個不同的 Anchor,來獲取月份標籤的bounds, 其中一個左上角的Point 一個是右下角的 Point。而不是用Anchor。
提醒一下,使用Anchor是對這種特定問題的一個更好的解決方案。然而,我們用CGPoint方案只是為了知道如何獲取一個視圖的多個錨定偏好。

首先修改MyTextPreferenceData來容納兩個極端rect,要設置成可選型, 因為它們不能同時賦值。

struct MyTextPreferenceData {
let viewIdx: Int
var topLeading: Anchor<CGPoint>? = nil
var bottomTrailing: Anchor<CGPoint>? = nil
}

PreferenceKey 保持不變。

struct MyTextPreferenceKey: PreferenceKey {
typealias Value = [MyTextPreferenceData]
static var defaultValue: [MyTextPreferenceData] = []
static func reduce(value: inout [MyTextPreferenceData], nextValue: () -> [MyTextPreferenceData]) {
value.append(contentsOf: nextValue())
}
}

月份標籤沒必要設置兩個錨定偏好,但是如果我們在同一個視圖中多次調用.anchorPreference()。 只有最後一次起作用。 相反我們需要調用 .anchorPreference(), 然後再調用.transformAnchorPreference(),來補回缺失的信息。

struct MonthView: View {
@Binding var activeMonth: Int
let label: String
let idx: Int
var body: some View {
Text(label)
.padding(10)
.anchorPreference(key: MyTextPreferenceKey.self, value: .topLeading, transform: { [MyTextPreferenceData(viewIdx: self.idx, topLeading: $0)] })
.transformAnchorPreference(key: MyTextPreferenceKey.self, value: .bottomTrailing, transform: { ( value: inout [MyTextPreferenceData], anchor: Anchor<CGPoint>) in
value[0].bottomTrailing = anchor
})
.onTapGesture { self.activeMonth = self.idx }
}
}

最後,我們相應的更新.createBorder(),所以它使用的是兩個point來進行的計算,而不是rect.

        struct ContentView : View {
@State private var activeIdx: Int = 0
var body: some View {
VStack {
Spacer()
HStack {
MonthView(activeMonth: $activeIdx, label: "January", idx: 0)
MonthView(activeMonth: $activeIdx, label: "February", idx: 1)
MonthView(activeMonth: $activeIdx, label: "March", idx: 2)
MonthView(activeMonth: $activeIdx, label: "April", idx: 3)
}
Spacer()
HStack {
MonthView(activeMonth: $activeIdx, label: "May", idx: 4)
MonthView(activeMonth: $activeIdx, label: "June", idx: 5)
MonthView(activeMonth: $activeIdx, label: "July", idx: 6)
MonthView(activeMonth: $activeIdx, label: "August", idx: 7)
}
Spacer()
HStack {
MonthView(activeMonth: $activeIdx, label: "September", idx: 8)
MonthView(activeMonth: $activeIdx, label: "October", idx: 9)
MonthView(activeMonth: $activeIdx, label: "November", idx: 10)
MonthView(activeMonth: $activeIdx, label: "December", idx: 11)
}
Spacer()
}.backgroundPreferenceValue(MyTextPreferenceKey.self) { preferences in
return GeometryReader { geometry in
ZStack {
self.createBorder(geometry, preferences)
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
}
func createBorder(_ geometry: GeometryProxy, _ preferences: [MyTextPreferenceData]) -> some View {
let p = preferences.first(where: { $0.viewIdx == self.activeIdx })
let aTopLeading = p?.topLeading
let aBottomTrailing = p?.bottomTrailing
let topLeading = aTopLeading != nil ? geometry[aTopLeading!] : .zero
let bottomTrailing = aBottomTrailing != nil ? geometry[aBottomTrailing!] : .zero
return RoundedRectangle(cornerRadius: 15)
.stroke(lineWidth: 3.0)
.foregroundColor(Color.green)
.frame(width: bottomTrailing.x - topLeading.x, height: bottomTrailing.y - topLeading.y)
.fixedSize()
.offset(x: topLeading.x, y: topLeading.y)
.animation(.easeInOut(duration: 1.0))
}
}

嵌套視圖

到目前為止,我們已經在兄弟視圖上使用了preferences。但在嵌套視圖的使用上我們還有更多的挑戰。.transformAnchorPreference() 就變的很重要了,如果你有嵌套的兩個視圖,而且兩個都設置.anchorPreference(),子級視圖的將不會起作用。 為了解決這樣個問題,你必須要指定子級視圖的anchorPreference和父級視圖的transformAnchorPreference。但是別慌, 我們會詳細介紹的。

下一步是什麼

在這一系列的最後一部分,將用一個不同的例子來說明。 我們將會有一個小的示意圖。小的示意圖將會讀取視圖樹的表單來構造。 我們將會去修改表單的視圖。而且會馬上生效。它只是對這個表單視圖樹的preferences改變產生了反饋。

這裡有個小圖來解釋:

[SwiftUILab]探究View樹part2AnchorPreferences(錨定偏好)

我相信這個系列的最後一部分你會來。如果你想要被提醒一下,可以在Twitter上關注我,下次見啦~

相關文章

品HashMap(java8)

SpringSecurity框架下實現CSRF跨站攻擊防禦

Kubernetes時代的安全軟件供應鏈

ThreadLocal介紹