[SwiftUI-Lab] 探究View樹(shù) part-3 嵌套視圖

文章源地址:[https://swiftui-lab.com/communicating-with-the-view-tree-part-3/)

作者: Javier

翻譯: Liaoworking

處理嵌套視圖的偏好(Preferences)

在之前的部分我們介紹了SwiftUI的錨定偏好(anchor preferences) , 現(xiàn)在到了最后一部分啦~ 把所有知識(shí)點(diǎn)放到一起呆奕,繼續(xù)學(xué)習(xí)SwiftUI處理嵌套視圖的偏好(preferences)。順便添加一些Anchor<T>的其他用法。就從下面的例子開(kāi)始吧课锌。

我們的目標(biāo)是創(chuàng)建一個(gè)小的示意圖來(lái)展示一些狀態(tài)的表格浩村。


image

在例子中需要注意:

  • 左邊小的示意圖是縮小版的右邊表格犀忱。不同的顏色代表不同的title view, 文本字段和文本字段的容器得院。
  • 隨著文字的變多左电,小示意圖也會(huì)表現(xiàn)出來(lái)牌废。
  • 當(dāng)我們添加新的視圖的時(shí)候咽白,小示意圖也會(huì)改變。
  • 當(dāng)表格的frame改變的時(shí)候鸟缕, 小示意圖也會(huì)改變晶框。
  • 文本框的顏色紅色代表沒(méi)有輸入,黃色小于3個(gè)懂从,綠色大于等于3個(gè)授段。

注意小示意圖并不知道任何關(guān)于表單的信息,它只是因?yàn)橐晥D層級(jí)的偏好(preferences)的改變而改變番甩。

那就開(kāi)始吧~

因?yàn)檫@里的視圖有多種類型侵贵,需要來(lái)將它們區(qū)分,這里我們用一個(gè)枚舉缘薛。

enum MyViewType: Equatable {
    case formContainer // 主容器
    case fieldContainer // 包括標(biāo)簽和文本框的容器
    case field(Int) //文本框窍育,包括內(nèi)容長(zhǎng)度        
    case title // 表單標(biāo)題
    case miniMapArea // 小示意圖后面的視圖
}

然后我們定義一個(gè)MyPreferenceData類卡睦,用來(lái)處理偏好中設(shè)置的數(shù)據(jù)。它有兩個(gè)屬性(vtype 和 bounds)漱抓,還有一些會(huì)使用到的方法:

struct MyPreferenceData: Identifiable {
    let id = UUID() // forEach必須要用的屬性
    let vtype: MyViewType
    let bounds: Anchor<CGRect>
    
    // 配置小示意圖的顏色
    func getColor() -> Color {
        switch vtype {
        case .field(let length):
            return length == 0 ? .red : (length < 3 ? .yellow : .green)
        case .title:
            return .purple
        default:
            return .gray
        }
    }
    
    // 如果當(dāng)前view必須要顯示在小示意圖中 返回true
    // 只有文本框 文本框容器 標(biāo)題才顯示在小示意圖中
    func show() -> Bool {
        switch vtype {
        case .field:
            return true
        case .title:
            return true
        case .fieldContainer:
            return true
        default:
            return false
        }
    }
}

定義PreferenceKey

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

有趣的地方開(kāi)始了表锻,我們有很多的區(qū)域,每一個(gè)區(qū)域前面都有一個(gè)textLabel乞娄,并被一個(gè)容器包裹瞬逊。我們把這些重復(fù)的視圖封裝成MyFormField類。與此同時(shí)也相應(yīng)的設(shè)置好偏好补胚。文本框是VStack的一部分码耐。我們需要兩個(gè)嵌套視圖的bounds. 不可以使用.anchorPreference()兩次,在VStack中調(diào)用anchorPreference()會(huì)阻止TextField中的調(diào)用溶其∩龋可以在VStack中掉用.transformAnchorPreference()來(lái)添加數(shù)據(jù),而不是替換數(shù)據(jù)瓶逃。

// 包含標(biāo)題束铭、文本框的圓角視圖
struct MyFormField: View {
    @Binding var fieldValue: String
    let label: String
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(label)
            TextField("", text: $fieldValue)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .anchorPreference(key: MyPreferenceKey.self, value: .bounds) {
                    return [MyPreferenceData(vtype: .field(self.fieldValue.count), bounds: $0)]
                }
        }
        .padding(15)
        .background(RoundedRectangle(cornerRadius: 15).fill(Color(white: 0.9)))
        .transformAnchorPreference(key: MyPreferenceKey.self, value: .bounds) {
            $0.append(MyPreferenceData(vtype: .fieldContainer, bounds: $1))
        }
    }
}

ContentView把所有的View都放到了一起,這里我們?cè)O(shè)置了三個(gè)偏好厢绝,稍后會(huì)再小示意圖中用到契沫。這三個(gè)偏好分別存儲(chǔ)的是圖表標(biāo)題的bounds,圖表區(qū)域和示意圖區(qū)域昔汉。

struct ContentView : View {
    @State private var fieldValues = Array<String>(repeating: "", count: 5)
    @State private var length: Float = 360
    @State private var twitterFieldPreset = false
    
    var body: some View {
        
        VStack {
            Spacer()
            
            HStack(alignment: .center) {
                
                // 存放小示意圖的View
                // 我們將獲取它的大小懈万、位置來(lái)確定小示意圖的元素正確顯示
                Color(white: 0.7)
                    .frame(width: 200)
                    .anchorPreference(key: MyPreferenceKey.self, value: .bounds) {
                        return [MyPreferenceData(vtype: .miniMapArea, bounds: $0)]
                    }
                    .padding(.horizontal, 30)
                
                // 表單容器
                VStack(alignment: .leading) {
                    // 標(biāo)題
                    VStack {
                        Text("Hello \(fieldValues[0]) \(fieldValues[1]) \(fieldValues[2])")
                            .font(.title).fontWeight(.bold)
                            .anchorPreference(key: MyPreferenceKey.self, value: .bounds) {
                                return [MyPreferenceData.init(vtype: .title, bounds: $0)]
                        }
                        Divider()
                    }
                    
                    // 開(kāi)關(guān)和滑條
                    HStack {
                        Toggle(isOn: $twitterFieldPreset) { Text("") }
                        
                        Slider(value: $length, in: 360...540).layoutPriority(1)
                    }.padding(.bottom, 5)

                    // 文本框的第一行
                    HStack {
                        MyFormField(fieldValue: $fieldValues[0], label: "First Name")
                        MyFormField(fieldValue: $fieldValues[1], label: "Middle Name")
                        MyFormField(fieldValue: $fieldValues[2], label: "Last Name")
                    }.frame(width: 540)
                    
                    // 文本框的第二行
                    HStack {
                        MyFormField(fieldValue: $fieldValues[3], label: "Email")
                        
                        if twitterFieldPreset {
                            MyFormField(fieldValue: $fieldValues[4], label: "Twitter")
                        }
                        
                        
                    }.frame(width: CGFloat(length))

                }.transformAnchorPreference(key: MyPreferenceKey.self, value: .bounds) {
                    $0.append(MyPreferenceData(vtype: .formContainer, bounds: $1))
                }

                Spacer()
                
            }
            .overlayPreferenceValue(MyPreferenceKey.self) { preferences in
                GeometryReader { geometry in
                    MiniMap(geometry: geometry, preferences: preferences)
                }
            }
            
            Spacer()
        }.background(Color(white: 0.8)).edgesIgnoringSafeArea(.all)
    }
}

最后,我們的小示意圖會(huì)遍歷所有的偏好靶病,并繪制小示意圖中的每一個(gè)元素会通。

struct MiniMap: View {
    let geometry: GeometryProxy
    let preferences: [MyPreferenceData]
    
    var body: some View {
        // 獲得表單容器的偏好
        guard let formContainerAnchor = preferences.first(where: { $0.vtype == .formContainer })?.bounds else { return AnyView(EmptyView()) }
        
        // 獲得小示意圖的偏好
        guard let miniMapAreaAnchor = preferences.first(where: { $0.vtype == .miniMapArea })?.bounds else { return AnyView(EmptyView()) }
        
        // 計(jì)算表單的數(shù)據(jù) 用來(lái)顯示在小示意圖中
        let factor = geometry[formContainerAnchor].size.width / (geometry[miniMapAreaAnchor].size.width - 10.0)
        
        // 確定表單的位置
        let containerPosition = CGPoint(x: geometry[formContainerAnchor].minX, y: geometry[formContainerAnchor].minY)
        
        // 確定小示意圖的位置
        let miniMapPosition = CGPoint(x: geometry[miniMapAreaAnchor].minX, y: geometry[miniMapAreaAnchor].minY)

        // -------------------------------------------------------------------------------------------------
        // iOS 13 Beta 5 正式版發(fā)布日志 已知問(wèn)題:
        // 復(fù)雜的ForEach view可能會(huì)有編譯報(bào)錯(cuò)
        // 解決方案: 抽一個(gè)新的View出來(lái)
        // -------------------------------------------------------------------------------------------------
        // 由于 beta 5編譯報(bào)錯(cuò)的bug,封裝成AnyView.
        return AnyView(miniMapView(factor, containerPosition, miniMapPosition))
    }

    func miniMapView(_ factor: CGFloat, _ containerPosition: CGPoint, _ miniMapPosition: CGPoint) -> some View {
        ZStack(alignment: .topLeading) {
            // 創(chuàng)建小的示意圖視圖
            // 首選項(xiàng)以相反的順序遍歷
            // 將被父視圖覆蓋
            ForEach(preferences.reversed()) { pref in
                if pref.show() { // 一些不想要顯示的視圖
                    self.rectangleView(pref, factor, containerPosition, miniMapPosition)
                }
            }
        }.padding(5)
    }
    
    func rectangleView(_ pref: MyPreferenceData, _ factor: CGFloat, _ containerPosition: CGPoint, _ miniMapPosition: CGPoint) -> some View {
        Rectangle()
        .fill(pref.getColor())
        .frame(width: self.geometry[pref.bounds].size.width / factor,
               height: self.geometry[pref.bounds].size.height / factor)
        .offset(x: (self.geometry[pref.bounds].minX - containerPosition.x) / factor + miniMapPosition.x,
                y: (self.geometry[pref.bounds].minY - containerPosition.y) / factor + miniMapPosition.y)
    }

}

關(guān)于視圖樹(shù)的一句話

現(xiàn)在讓我們停下來(lái)思考一下:偏好(preference)閉包在嵌套視圖中的執(zhí)行順序娄周。例如先看看小示意圖的實(shí)現(xiàn)涕侈,你可能會(huì)注意到ForEach以相反的順序運(yùn)行循環(huán),否則文本框容器會(huì)最后才繪制煤辨,來(lái)覆蓋對(duì)應(yīng)的小示意圖的文本框裳涛。 所以了解偏好的設(shè)置就變的很重要了。

首先請(qǐng)注意:并沒(méi)有關(guān)于SwiftUI遍歷視圖樹(shù)順序的文檔众辨,PreferenceKey類中的reduce方法聲明中提到值是以視圖樹(shù)的順序排列端三。但是沒(méi)有告訴我們這個(gè)順序是什么,我們能確認(rèn)的是這并不是隨機(jī)順序而且每次刷新的時(shí)候都保持一致泻轰。
下面我講的所有和關(guān)于閉包中的運(yùn)行順序相關(guān)的技肩,都是通過(guò)專門的實(shí)驗(yàn)弄清楚的。我?guī)缀趺總€(gè)地方都打斷點(diǎn)了浮声,都說(shuō)的通虚婿,我也對(duì)此很有信心

下面的圖表簡(jiǎn)單的說(shuō)明了視圖層級(jí),為了讓視圖更容易理解泳挥,簡(jiǎn)單的視圖就忽略了然痊,紅色的箭頭表示anchorPreference()transformAnchorPreference() 閉包的執(zhí)行順序。注意屉符,只有SwiftUI認(rèn)為必須的才會(huì)調(diào)用剧浸,并不是所有的閉包都會(huì)被調(diào)用。例如視圖的bounds并沒(méi)有改變.anchorPreference()可能就不會(huì)調(diào)用矗钟。當(dāng)不確定的時(shí)候唆香,打個(gè)斷點(diǎn)或者打印一下?tīng)顟B(tài)來(lái)調(diào)試一下。

image

如圖吨艇,SwiftUI遵循下面兩個(gè)原則:

  • 1.同級(jí)的視圖的遍歷順序和它們?cè)诖a中的出現(xiàn)順序相同躬它。
  • 2.子級(jí)視圖的閉包執(zhí)行時(shí)機(jī)要比父級(jí)視圖早。

Anchor<T>的其他使用东涡。

正如我們所看到的冯吓,Anchor<T>.Source可以被一下靜態(tài)變量獲得到,如.bounds, .topLeading, .bottom等疮跑。我們通常將它傳遞給anchorPreference() 修改器组贺。然而你可以通過(guò)靜態(tài)變量Anchor<T>.Source創(chuàng)建你自己的Anchor<CGRect>.Source和Anchor<CGPoint>.Source,如下:

let a1 = Anchor<CGRect>.Source.rect(CGRect(x: 0, y: 0, width: 100, height: 50))
let a2 = Anchor<CGPoint>.Source.point(CGPoint(x: 10, y: 30))
let a3 = Anchor<CGPoint>.Source.unitPoint(UnitPoint(x: 10, y: 30))

你可能會(huì)問(wèn)我們什么時(shí)候用這些祖娘,你可以把值傳遞給偏好失尖,如果已存在的靜態(tài)變量對(duì)你都沒(méi)啥用處,但是當(dāng)處理彈框的時(shí)候用它就會(huì)特別的方便渐苏。

.popover(isPresented: $showPopOver,
         attachmentAnchor: .rect(Anchor<CGRect>.Source.rect(CGRect(x: 0, y: 0, width: 100, height: 50))),
         arrowEdge: .leading) { ... }

總結(jié):

恭喜掀潮,這一系列總算到最后了,希望你能享受享受這些工具而且用來(lái)創(chuàng)作炫酷的app整以。有著無(wú)限的可能性胧辽。歡迎評(píng)論,或者給我email和Twitter上關(guān)注我公黑。

歡迎關(guān)注來(lái)獲取更多文章邑商。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市凡蚜,隨后出現(xiàn)的幾起案子人断,更是在濱河造成了極大的恐慌,老刑警劉巖朝蜘,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恶迈,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)暇仲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門步做,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人奈附,你說(shuō)我怎么就攤上這事全度。” “怎么了斥滤?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵将鸵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我佑颇,道長(zhǎng)顶掉,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任挑胸,我火速辦了婚禮痒筒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嗜暴。我一直安慰自己凸克,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布闷沥。 她就那樣靜靜地躺著萎战,像睡著了一般。 火紅的嫁衣襯著肌膚如雪舆逃。 梳的紋絲不亂的頭發(fā)上蚂维,一...
    開(kāi)封第一講書(shū)人閱讀 51,370評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音路狮,去河邊找鬼虫啥。 笑死,一個(gè)胖子當(dāng)著我的面吹牛奄妨,可吹牛的內(nèi)容都是我干的涂籽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼砸抛,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼评雌!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起直焙,我...
    開(kāi)封第一講書(shū)人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤景东,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后奔誓,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體斤吐,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了和措。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片庄呈。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖臼婆,靈堂內(nèi)的尸體忽然破棺而出抒痒,到底是詐尸還是另有隱情幌绍,我是刑警寧澤颁褂,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站傀广,受9級(jí)特大地震影響颁独,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜伪冰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一誓酒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧贮聂,春花似錦靠柑、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至耻警,卻和暖如春隔嫡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背甘穿。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工腮恩, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人温兼。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓秸滴,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親募判。 傳聞我的和親對(duì)象是個(gè)殘疾皇子荡含,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容