學(xué)習(xí)SwiftUI醇王,便繞不開視圖樹的概念,在接下來的4篇文章中搁吓,我會(huì)帶領(lǐng)大家學(xué)習(xí)相關(guān)的概念原茅,通過對(duì)視圖樹的學(xué)習(xí),很多之前認(rèn)為很困難的問題堕仔,都會(huì)引刃而解擂橘。
視圖樹的概念不言而喻,在SwiftUI中摩骨,組成某個(gè)頁(yè)面的View的結(jié)構(gòu)是樹型的通贞,如下圖所示:
在SwiftUI中朗若,子view如果想獲取父view提供的數(shù)據(jù),一個(gè)最好的方式就是使用@EnvironmentObject
或者@Environment
,在這里只演示一個(gè)簡(jiǎn)單的例子:
@main
struct PreferenceKeyDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.myEnvironmentTestValue, 10.0)
}
}
}
struct MyEnvironmentKey: EnvironmentKey {
static var defaultValue: Double = 0.0
}
extension EnvironmentValues {
var myEnvironmentTestValue: Double {
get {
self[MyEnvironmentKey.self]
}
set {
self[MyEnvironmentKey.self] = newValue
}
}
}
上邊的代碼中昌罩,給ContentView設(shè)置了一個(gè)環(huán)境變量哭懈,然后我們?cè)谄渥觱iew中就可隨意獲取這個(gè)環(huán)境變量
struct ContentView: View {
var body: some View {
Example4()
}
}
struct Example4: View {
@Environment(\.myEnvironmentTestValue) var value: Double
var body: some View {
Text("\(value)")
}
}
后續(xù)我會(huì)專門寫一篇文章介紹這兩個(gè)知識(shí)點(diǎn)【ビ茫回到我們的話題遣总,如果父view想獲取其子view的一些數(shù)據(jù),怎么辦呢轨功?
大家頭腦中一定要對(duì)上圖中的問號(hào)有深刻的思考旭斥,只有這樣才能掌握在什么場(chǎng)景下需要使用本文講解的技術(shù)。
舉個(gè)??
struct Example1: View {
var body: some View {
NavigationView {
Text("Hello, world!")
.padding()
.navigationBarTitle("????????", displayMode: .inline)
}
}
}
大家看上邊這段代碼古涧,navigationBarTitle這個(gè)modifier寫在了Text上垂券,那么NavigationView是如何獲取到這些信息的呢?我們帶著這個(gè)疑問羡滑,在看一個(gè)??:
上圖中演示的功能很簡(jiǎn)單菇爪,點(diǎn)擊哪個(gè)數(shù)字,哪個(gè)數(shù)字就顯示一個(gè)border啄栓,用我們學(xué)過的知識(shí)就能實(shí)現(xiàn)這個(gè)功能娄帖,代碼如下:
struct Example2: View {
@State private var activeNumber: Int = 1
var body: some View {
VStack {
Spacer()
HStack {
NumberView(activeNumber: $activeNumber, number: 1)
NumberView(activeNumber: $activeNumber, number: 2)
NumberView(activeNumber: $activeNumber, number: 3)
}
HStack {
NumberView(activeNumber: $activeNumber, number: 4)
NumberView(activeNumber: $activeNumber, number: 5)
NumberView(activeNumber: $activeNumber, number: 6)
}
HStack {
NumberView(activeNumber: $activeNumber, number: 7)
NumberView(activeNumber: $activeNumber, number: 8)
NumberView(activeNumber: $activeNumber, number: 9)
}
Spacer()
}
}
struct NumberView: View {
@Binding var activeNumber: Int
let number: Int
var body: some View {
Text("\(number)")
.font(.system(size: 40, weight: .heavy, design: .rounded))
.padding(20)
.background(NumberBorder(show: activeNumber == number))
.onTapGesture {
self.activeNumber = number
}
}
}
struct NumberBorder: View {
let show: Bool
var body: some View {
Circle()
.stroke(show ? Color.green : Color.clear, lineWidth: 5)
.animation(.easeInOut)
}
}
}
核心思想就是也祠,父view記錄一個(gè)activeNumber昙楚,然后為index等于activeNumber的NumberView的border設(shè)置顏色。我們把難度稍為提高一點(diǎn)诈嘿,要求實(shí)現(xiàn)下圖的功能:
最明顯的改變就是堪旧,只有一個(gè)綠色圓圈在執(zhí)行動(dòng)畫,仔細(xì)思考奖亚,我們發(fā)現(xiàn)淳梦,要實(shí)現(xiàn)上述功能,需要父view獲取子view的位置信息昔字,這恰恰引出了本文的核心內(nèi)容:父類如何獲取子view的信息爆袍。
關(guān)于這個(gè)問題,我們可以想像成子view可以把自己的一些信息先打包作郭,然后和自身綁定陨囊,父view就能獲取到這些包裹。
那么夹攒,如何打包呢蜘醋?
struct NumberPreferenceValue: Equatable {
let viewIdx: Int
let rect: CGRect
}
很簡(jiǎn)單,把需要傳遞的信息封裝成一個(gè)結(jié)構(gòu)體就行了咏尝,但需要實(shí)現(xiàn)Equatable協(xié)議压语,在本例中啸罢,我們打包了兩個(gè)信息,原則上可以打包任何信息胎食。
struct NumberPreferenceViewSetter: View {
let idx: Int
var body: some View {
GeometryReader { proxy in
Circle()
.stroke(Color.clear, lineWidth: 5)
.preference(key: NumberPreferenceKey.self, value: [NumberPreferenceValue(viewIdx: idx, rect: proxy.frame(in: .named("ZStackSpace")))])
}
}
}
我們?yōu)槊總€(gè)子view添加了一個(gè)透明的邊框扰才,通過preference這個(gè)modifier綁定自身的信息,注意厕怜,preference要求傳入一個(gè)key和value:
struct NumberPreferenceKey: PreferenceKey {
typealias Value = [NumberPreferenceValue]
static var defaultValue: [NumberPreferenceValue] = []
static func reduce(value: inout [NumberPreferenceValue], nextValue: () -> [NumberPreferenceValue]) {
value.append(contentsOf: nextValue())
}
}
- key: 只需要實(shí)現(xiàn)PreferenceKey協(xié)議即可训桶,該協(xié)議要求實(shí)現(xiàn)一個(gè)靜態(tài)變量defaultValue和靜態(tài)函數(shù)reduce
- value:就是我們上邊封裝好的結(jié)構(gòu)體,在本例中酣倾,我們把NumberPreferenceValue放到了數(shù)組中
其實(shí)舵揭,這些都是固定寫法,當(dāng)父view想要獲取子view信息的時(shí)候躁锡,他就會(huì)遍歷子view中的reduce午绳,然后把所有的包裹合并成一個(gè)數(shù)組。
var body: some View {
ZStack(alignment: .topLeading) {
...
VStack {
...
}
}
.onPreferenceChange(NumberPreferenceKey.self) { preferences in
for pre in preferences {
self.rects[pre.viewIdx] = pre.rect
}
}
.coordinateSpace(name: "ZStackSpace")
ZStack通過.onPreferenceChange
獲取了全部的preferences映之,然后根據(jù)包裹中的數(shù)據(jù)給self.rects賦值拦焚。這樣就實(shí)現(xiàn)了上述的功能。
完整代碼如下:
struct Example3: View {
@State private var activeNumber: Int = 1
@State private var rects: [CGRect] = Array<CGRect>(repeating: CGRect(), count: 9)
var body: some View {
ZStack(alignment: .topLeading) {
Circle()
.stroke(Color.green, lineWidth: 5)
.frame(width: rects[activeNumber - 1].width, height: rects[activeNumber - 1].height)
.offset(x: rects[activeNumber - 1].minX, y: rects[activeNumber - 1].minY)
.animation(.easeInOut)
VStack {
Spacer()
HStack {
NumberView(activeNumber: $activeNumber, number: 1)
NumberView(activeNumber: $activeNumber, number: 2)
NumberView(activeNumber: $activeNumber, number: 3)
}
HStack {
NumberView(activeNumber: $activeNumber, number: 4)
NumberView(activeNumber: $activeNumber, number: 5)
NumberView(activeNumber: $activeNumber, number: 6)
}
HStack {
NumberView(activeNumber: $activeNumber, number: 7)
NumberView(activeNumber: $activeNumber, number: 8)
NumberView(activeNumber: $activeNumber, number: 9)
}
Spacer()
}
}
.onPreferenceChange(NumberPreferenceKey.self) { preferences in
for pre in preferences {
self.rects[pre.viewIdx] = pre.rect
}
}
.coordinateSpace(name: "ZStackSpace")
}
struct NumberView: View {
@Binding var activeNumber: Int
let number: Int
var body: some View {
Text("\(number)")
.font(.system(size: 40, weight: .heavy, design: .rounded))
.padding(20)
.background(NumberPreferenceViewSetter(idx: number - 1))
.onTapGesture {
self.activeNumber = number
}
}
}
struct NumberPreferenceViewSetter: View {
let idx: Int
var body: some View {
GeometryReader { proxy in
Circle()
.stroke(Color.clear, lineWidth: 5)
.preference(key: NumberPreferenceKey.self, value: [NumberPreferenceValue(viewIdx: idx, rect: proxy.frame(in: .named("ZStackSpace")))])
}
}
}
struct NumberPreferenceValue: Equatable {
let viewIdx: Int
let rect: CGRect
}
struct NumberPreferenceKey: PreferenceKey {
typealias Value = [NumberPreferenceValue]
static var defaultValue: [NumberPreferenceValue] = []
static func reduce(value: inout [NumberPreferenceValue], nextValue: () -> [NumberPreferenceValue]) {
value.append(contentsOf: nextValue())
}
}
}
總結(jié)
當(dāng)某個(gè)場(chǎng)景下杠输,父view需要獲取子view的某些信息赎败,就可以考慮使用PreferenceKey這個(gè)技術(shù),它最大的優(yōu)點(diǎn)是可以讓子view封裝任何信息蠢甲。在本文的例子中僵刮,我們主要封裝的是子view的frame信息,這可能存在一些潛在的問題鹦牛,比如搞糕,如果父view的布局改變了,影響到了子view的布局曼追,子view的布局又影響了父view的布局窍仰,這種情況下可能會(huì)出現(xiàn)死循環(huán)。
本文示例代碼:SwiftUI-PreferenceKeyDemo.swift
SwiftUI集合:FuckingSwiftUI