国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

SwiftUI 布局協(xié)議 - Part1

這篇具有很好參考價值的文章主要介紹了SwiftUI 布局協(xié)議 - Part1。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

簡介

今年 SwiftUI 新增最好的功能之一必須是布局協(xié)議。它不但讓我們參與到布局過程中,而且也給了我們一個很好的機(jī)會去更好的理解布局在 SwiftUI 中的作用。

早在2019年,我寫了一篇文章 SwiftUI 中 frame 的表現(xiàn),其中,我闡述了父視圖和子視圖如何協(xié)調(diào)形成最終視圖效果。那里描述的許多情況需要通過觀察不同測試的結(jié)果去猜測。整個過程就像是發(fā)現(xiàn)外星行星,天文學(xué)家發(fā)現(xiàn)太陽亮度微小的減少,然后推斷出這一定是行星過境(了解行星過境)。

現(xiàn)在,有了布局協(xié)議,就像用自己的眼睛在遙遠(yuǎn)的太陽系漫游,令人振奮。

創(chuàng)建一個基礎(chǔ)布局并不難,只需要實(shí)現(xiàn)兩個方法。盡管如此,我們?nèi)匀挥泻芏噙x擇去實(shí)現(xiàn)一個復(fù)雜的容器。我們將會探索常規(guī)布局案例之外的內(nèi)容。有許多有趣的話題到目前為止我還沒有在任何地方看到過解釋,所以我將在這里介紹它們。然而,在深入這些領(lǐng)域之前,我們需要先打下扎實(shí)的基礎(chǔ)。

由于涉及到許多內(nèi)容,我將分成兩個部分:

Part 1 - 基礎(chǔ):

  • 什么是布局協(xié)議
  • 視圖層次結(jié)構(gòu)的族動態(tài)
  • 我們的第一個布局實(shí)現(xiàn)
  • 容器對齊
  • 自定義值:LayoutValueKey
  • 默認(rèn)間距
  • 布局屬性和 Spacer()
  • 布局緩存
  • 高明的偽裝者
  • 使用AnyLayout切換布局
  • 結(jié)語

Part 2 - 高級布局:

  • 開啟有趣的旅程
  • 自定義動畫
  • 雙向自定義值
  • 避免布局循環(huán)和崩潰
  • 遞歸布局
  • 布局組合
  • 另一個組合案例:插入兩個布局
  • 使用綁定參數(shù)
  • 一個有用的調(diào)試工具
  • 最后的思考

如果你已經(jīng)熟悉布局協(xié)議,你可能想直接跳到第二部分。這是可以的,盡管我仍然推薦你瀏覽第一部分,至少淺讀一下。這將確保我們在開始探索第二部分中描述的更多高級特性時,我們在同一進(jìn)度。

如果在閱讀本文的任何時候,你認(rèn)為布局協(xié)議不適合你(至少目前來說),我仍然建議你查看 Part2 的這一小節(jié)—一個有用的調(diào)試工具,這個工具可以幫助你使用 SwiftUI ,且不需要理解布局協(xié)議就可以使用。我將它放在第二部分結(jié)尾是有原因的,這個工具是使用本文的知識構(gòu)建的。不過,你可以直接復(fù)制代碼使用它。

什么是布局協(xié)議

采用布局協(xié)議類型的任務(wù),是告訴 SwiftUI 如何放置一組視圖,需要多少空間。這類型常常被作為視圖容器,雖然布局協(xié)議是今年新推出的(至少公開來說),但是我們在第一天使用 SwiftUI 的時候就在使用了,當(dāng)每次使用 HStack 或者 VStack 放置視圖時都是如此。

請注意至少到現(xiàn)在,布局協(xié)議不能創(chuàng)建懶加載容器,比如 LazyHStackLazyVStack。懶加載容器是指那些只在滾入屏幕時渲染,滾出到屏幕外就停止渲染的視圖。

一個重要的知識點(diǎn),Layout 類型不是視圖 。例如,它們沒有視圖擁有的 body 屬性。但是不用擔(dān)心,目前為止你可以認(rèn)為它們就是視圖并且像視圖一樣使用它們。這個框架使用了漂亮的 Swift 語言技巧使你的布局代碼在向 SwiftUI 中插入時產(chǎn)生一個透明視圖 。我將在后面-高明的偽裝者部分說明。

視圖層次結(jié)構(gòu)的族動態(tài)

在我們開始布局代碼之前,讓我們重新審視一下 SwiftUI 框架的核心。就像我在以前的文章 SwiftUI 中 frame 的表現(xiàn) 所描述的的那樣,在布局過程中,父視圖給子視圖提供一個尺寸,但最終還是由子視圖決定如何繪制自己。然后,它將此傳達(dá)給父視圖,以便采取相應(yīng)的動作。有三個可能的情況,我們將專注討論于橫軸(寬度),但縱軸(高度)同理:

情況一:如果子視圖需求小于提供的視圖

在這個例子中考慮文本視圖,提供了比需要繪制文字更多的空間

SwiftUI 布局協(xié)議 - Part1

struct ContentView: View {
    var body: some View {
        HStack(spacing: 0) {

            Rectangle().fill(.green)

            Text("Hello World!")

            Rectangle().fill(.green)

        }
        .padding(20)
    }
}

在這個例子中,屏幕寬度是 400pt。因此,文本提供 HStack 寬度的三分之一 ((400 – 40) / 3 = 120)。在這 120pt 中,文本只需要 74,并傳達(dá)給父視圖,父視圖現(xiàn)在可以拿走多余的 46pt 給其他的子視圖用。因?yàn)槠渌右晥D是圖形,所以它們可以接收給它們的一切東西。在這種情況下,120+46/2=143。

情況二:如果子視圖完全接收提供的視圖

圖形就是視圖中的一個例子,不管你提供了什么他都能接收。在上一個例子中,綠色矩形占據(jù)了提供的所有空間,但沒有一個多余的像素。

情況三:如果子視圖需求超出提供的視圖

思考下面這個例子,圖片視圖特別嚴(yán)格(除非他們修改了 resizable 方法),它們需要多少空間就要占用多少空間,在下面這個例子中,圖片是 300×300,這也是它們需要繪制自己需要的空間,然而,通過調(diào)用 frame(width:100) 子視圖只得到了 100pt,父視圖就沒有辦法只能聽從子視圖的做法嗎?并非如此,子視圖仍然會使用 300pt 繪制,但是父視圖將會布局其他視圖,就好像子視圖只有 100pt 寬度一樣。結(jié)果呢,我們將會有一個超出邊界的子視圖,但是周圍的視圖不會被圖片額外使用的空間影響。在下面這個例子中,黑色邊框展示的空間是提供給圖片的。

SwiftUI 布局協(xié)議 - Part1

struct ContentView: View {
    var body: some View {
        HStack(spacing: 0) {

            Rectangle().fill(.yellow)

            Image("peach")
                .frame(width: 100)
                .border(.black, width: 3)
                .zIndex(1)

            Rectangle().fill(.yellow)

        }
        .padding(20)
    }
}

視圖的行為方式有很多差異。例如,我們看見文本獲取需求空間后如何處置多余的不需要的空間,然而,如果需求的空間大于提供,就可能會發(fā)生一些事情,具體取決于你如何配置你的視圖。例如,可能會根據(jù)提供的尺寸截取文本,或者在提供的寬度內(nèi)垂直的展示文本,如果你使用 fixedSize 修改甚至可能超出屏幕就像例子中的圖片一樣。請記住, fixedSize 告訴視圖使用其理想尺寸,無論提供的是多少。

如果你想了解更多這些行為以及如何改變它們,請查看我以前的文章 SwiftUI 中 frame 的表現(xiàn)

我們的第一個布局實(shí)現(xiàn)

創(chuàng)建一個布局類型需要我們實(shí)現(xiàn)至少兩個方法, sizeThatFitsplaceSubviews 。這些方法接收一些新類型作為參數(shù): ProposedViewSizeLayoutSubview 。在我們開始寫方法之前,先看看這些參數(shù)長什么樣:

ProposedViewSize

ProposedViewSize 被父視圖用來告知子視圖如何計(jì)算自己的尺寸。這是一個簡單的類型,但很強(qiáng)大。它只是一對可選的 CGFloat ,用于建議寬度和高度。然而,正是我們?nèi)绾谓忉屵@些值才使它們變得有趣。

這些屬性可以有具體的值(例如35,74等),但當(dāng)它們等于0.0 ,nil 或者 .infinity 時是有特殊的含義。

  • 對于一個具體的寬度,例如 45,父視圖提供的也是 45pt,這個視圖應(yīng)該由提供的寬度來決定自身的尺寸
  • 對于寬度為 0.0,子視圖應(yīng)該響應(yīng)為最小尺寸
  • 對于寬度為 .infinity ,子視圖應(yīng)該響應(yīng)為最大尺寸
  • 對于 nil,父視圖應(yīng)該響應(yīng)為理想尺寸

ProposedViewSize 也可以有一些預(yù)定義值:

ProposedViewSize.zero = ProposedViewSize(width: 0, height: 0)
ProposedViewSize.infinity = ProposedViewSize(width: .infinity, height: .infinity)
ProposedViewSize.unspecified = ProposedViewSize(width: nil, height: nil)

LayoutSubview

sizeTheFitsplaceSubviews 方法也接收一個 Layout.Subviews 參數(shù),它是一個 LayoutSubview 元素的合集。每個視圖都有一個,作為父視圖的直接后代。盡管有這個名稱,但它的類型不是視圖,而是一個代理。我們可以查詢這些代理去了解我們正在布局的各個視圖的布局信息。例如,自 SwiftUI 推出以來,我們第一次可以直接查詢到視圖最小,理想和最大的尺寸,或者我們可以獲得每個視圖的布局優(yōu)先級以及其他有趣的值。

sizeThatFits 方法

func sizeThatFits(proposal: ProposedViewSize, subviews: Self.Subviews, cache: inout Self.Cache) -> CGSize

SwiftUI 將會調(diào)用 sizeThatFits 方法決定我們布局容器的尺寸,當(dāng)我們寫這個方法我們應(yīng)該認(rèn)為我們既是父視圖又是子視圖:當(dāng)作為父視圖時需要詢問子視圖的尺寸,當(dāng)我們是子視圖時,要基于我們子視圖的回復(fù)告訴父視圖需要的尺寸,

這個方法將會收到建議尺寸,一個子視圖代理的合集和一個緩存。最后一個參數(shù)可能用以提高我們的布局和一些其他高級應(yīng)用的性能,但現(xiàn)在我們不會使用它,我們會在后面一點(diǎn)再去看它。

當(dāng) sizeThatFits 方法在給定維度中(即寬度或高度)收到的建議尺寸為 nil 時,我們應(yīng)該返回容器的理想尺寸。當(dāng)收到的建議尺寸為0.0時,我們應(yīng)該返回容器的最小尺寸。當(dāng)收到的建議尺寸為 .infinity 時,我們應(yīng)該返回容器的最大尺寸。

注意 sizeThatFits 可能通過不同提案多次調(diào)用來測試容器的靈活性,提案可以是上述每個維度案例的任意組合。例如,你可能會得到一個帶有 ProposedViewSize(width: 0.0, height: .infinity)的調(diào)用。

在我們掌握了這些信息后,讓我們開始第一個布局。我們通過創(chuàng)建一個基礎(chǔ)的 HStack 開始。我們把它命名為 SimpleHStack 。為了比較兩者,我們創(chuàng)建一個標(biāo)準(zhǔn)的 HStack (藍(lán)色)視圖放置在SimpleHStack (綠色)上方。在我們的第一次嘗試中,我們將會實(shí)現(xiàn) sizeThatFits ,但是同時我們將會使其他需要的方法(placeSunviews)為空。

SwiftUI 布局協(xié)議 - Part1

struct ContentView: View {
    var body: some View {
        VStack(spacing: 20) {
            
            HStack(spacing: 5)  { 
                contents() 
            }
            .border(.blue)
            
            SimpleHStack(spacing: 5) {
                contents() 
            }
            .border(.blue)

        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.white)
    }
    
    @ViewBuilder func contents() -> some View {
        Image(systemName: "globe.americas.fill")
        
        Text("Hello, World!")

        Image(systemName: "globe.europe.africa.fill")
    }

}

struct SimpleHStack: Layout {
    let spacing: CGFloat
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let idealViewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
        
        let spacing = spacing * CGFloat(subviews.count - 1)
        let width = spacing + idealViewSizes.reduce(0) { $0 + $1.width }
        let height = idealViewSizes.reduce(0) { max($0, $1.height) }
        
        return CGSize(width: width, height: height)
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) 
    {
        // ...
    }
}

你可以觀察到,這兩個圖形的尺寸是一樣的。然而,這是因?yàn)槲覀儧]有在 placeSubviews 方法中編寫任何代碼,所有的視圖都放置在容器中間。如果你沒有明確的放置位置,這就是容器的默認(rèn)視圖。

在我們的 sizeThatFits 方法中,我們首先要計(jì)算每個視圖的所有理想尺寸。我們可以很容易的實(shí)現(xiàn),因?yàn)樽右晥D代理中有返回建議尺寸的方法。

一旦我們計(jì)算好所有理想尺寸,我們可以通過添加子視圖寬度和視圖間距來計(jì)算容器尺寸。從高度上來說,我們的視圖將會和最高子視圖一樣高。

你或許已經(jīng)察覺到了我們完全忽視了提供的尺寸,我們馬上回到這里,現(xiàn)在,讓我們實(shí)現(xiàn) placeSubviews 。

placeSubviews 方法

func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Self.Subviews, cache: inout Self.Cache)

SwiftUI 通過不同的提案值反復(fù)調(diào)用 sizeThatFits 來測試過容器視圖后,終于可以調(diào)用 placeSubviews 。在這里我們的目標(biāo)是遍歷子視圖,確定它們的位置并放置。

除了 sizeThatFits 收到同樣的參數(shù)外,placeSubviews 還得到一個 CGRect 參數(shù) 。bounds rect 具有我們在 sizeThatFits 方法中要求的尺寸。通常,矩形的原點(diǎn)是(0,0),但是你不應(yīng)該這樣假設(shè),如果我們正在組合布局,這個原點(diǎn)可能會有不同的值,我們將在后面看到。

放置視圖很簡單,這多虧了擁有放置方法的子視圖代理。我們必須提供視圖的坐標(biāo),錨點(diǎn)(默認(rèn)為中心)和建議尺寸,以便子視圖可以相應(yīng)地繪制自己。

struct SimpleHStack: Layout {
    
    // ...
    
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) 
    {
        var pt = CGPoint(x: bounds.minX, y: bounds.minY)
        
        for v in subviews {
            v.place(at: pt, anchor: .topLeading, proposal: .unspecified)
            
            pt.x += v.sizeThatFits(.unspecified).width + spacing
        }
    }
}

現(xiàn)在,還記得我之前提到的我們忽略了從父容器收到的建議了嗎?這意味著 SimpleHStack 容器將會一直擁有一樣的大小。不管提供什么,容器都會使用 .unspecified 計(jì)算尺寸和放置,意味著容器始終擁有理想的尺寸。在這個例子中容器的理想尺寸就是允許它以自己的理想尺寸放置所有子視圖的尺寸。如果我們改變提供尺寸看看會發(fā)生什么,在這個動畫中紅框代表提供的寬度。

SwiftUI 布局協(xié)議 - Part1

觀察 SimpleHStack 是如何忽視提供的尺寸并且總是以理想尺寸繪制自己,該尺寸適合所有子視圖的理想尺寸。

容器對齊

布局協(xié)議讓我們也為容器定義對齊指南。注意,這表明容器是作為一個整體如何與其余視圖對齊的。它對容器內(nèi)的視圖沒有任何影響。

在下面這個例子中,我們讓 SimpleHStack 對齊第二個視圖,但前提是容器與頭部對齊(如果把 VStack 的對齊方式改為尾部對齊,你將不會看到任何特殊的對齊方式)。

有紅色邊框的視圖是 SimpleHStack ,黑色邊框的視圖是標(biāo)準(zhǔn)的 HStack 容器,綠色邊框的表示封閉的 VStack

SwiftUI 布局協(xié)議 - Part1

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 5) {
            HStack(spacing: 5) {
                contents()
            }
            .border(.black)

            SimpleHStack(spacing: 5) {
                contents()
            }
            .border(.red)
            
            HStack(spacing: 5) {
                contents()
            }
            .border(.black)
            
        }
        .background { Rectangle().stroke(.green) }
        .padding()
        .font(.largeTitle)
            
    }
    
    @ViewBuilder func contents() -> some View {
        Image(systemName: "globe")
            .imageScale(.large)
            .foregroundColor(.accentColor)
        Text("Hello, world!")
    }
}
struct SimpleHStack: Layout {
    
    // ...

    func explicitAlignment(of guide: HorizontalAlignment, in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGFloat? {
        if guide == .leading {
            return subviews[0].sizeThatFits(proposal).width + spacing
        } else {
            return nil
        }
    }
}

優(yōu)先布局

當(dāng)我們使用 HStack 時,我們知道所有視圖都在平等的競爭寬度,除非它們有不同的布局優(yōu)先級。所有的視圖默認(rèn)優(yōu)先級都是0.0,但是,你可以通過調(diào)用 layoutPriority() 來修改布局優(yōu)先級。

執(zhí)行布局優(yōu)先級是容器布局的責(zé)任,所以如果我們創(chuàng)建一個新布局,如果相關(guān)的話,我們需要添加一些邏輯去考慮布局優(yōu)先級。我們?nèi)绾巫龅竭@一點(diǎn),這取決于我們自己。盡管有更好的方法(我們將在一分鐘內(nèi)解決它們),但你可以使用視圖布局優(yōu)先級的值賦予它們?nèi)魏我饬x。例如,在上一個例子中,我們將會根據(jù)視圖優(yōu)先級的值從左往右放置視圖。

為了實(shí)現(xiàn)效果,無需對子視圖集合進(jìn)行迭代,只需要簡單的通過優(yōu)先級排序。

truct SimpleHStack: Layout {
    
    // ...

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) 
    {
        var pt = CGPoint(x: bounds.minX, y: bounds.minY)
        
        for v in subviews.sorted(by: { $0.priority > $1.priority }) {
            v.place(at: pt, anchor: .topLeading, proposal: .unspecified)
            
            pt.x += v.sizeThatFits(.unspecified).width + spacing
        }
    }
}

在下面這個例子中,藍(lán)色圓圈將會首先出現(xiàn),因?yàn)樗绕鹌渌晥D擁有較高的優(yōu)先級

SwiftUI 布局協(xié)議 - Part1

SimpleHStack(spacing: 5) {
    Circle().fill(.yellow)
         .frame(width: 30, height: 30)
                        
    Circle().fill(.green)
        .frame(width: 30, height: 30)

    Circle().fill(.blue)
        .frame(width: 30, height: 30)
        .layoutPriority(1)
}

LayoutValueKey

自定義值:LayoutValueKey

不建議將布局優(yōu)先級用于優(yōu)先級以外的內(nèi)容,這可能使其他的用戶不理解你的容器,甚至將來的你也不理解。幸運(yùn)的是,我們有別的方法在視圖中添加新值。這個值并不限制于 CGFloat ,它們可以擁有任何類型(后面我們將在別的例子中看到)。

我們將重寫前面的例子,使用一個新值,我們把它稱為 PreferredPosition。第一件事就是創(chuàng)建一個符合LayoutValueKey 的類型,我們只需要一個帶有靜態(tài)默認(rèn)值的結(jié)構(gòu)體。這個默認(rèn)值用于沒有指明具體值的時候。

struct PreferredPosition: LayoutValueKey {
    static let defaultValue: CGFloat = 0.0
}

這樣,我們的視圖就擁有了新的屬性。為了設(shè)置這個值,我們需要用到 layoutValue() ,為了讀取這個值,我們使用 LayoutValueKey 類型作為視圖代理的下標(biāo):

SimpleHStack(spacing: 5) {
    Circle().fill(.yellow)
         .frame(width: 30, height: 30)
                        
    Circle().fill(.green)
        .frame(width: 30, height: 30)

    Circle().fill(.blue)
        .frame(width: 30, height: 30)
        .layoutValue(key: PreferredPosition.self, value: 1.0)
}
struct SimpleHStack: Layout {
    // ...

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) 
    {
        var pt = CGPoint(x: bounds.minX, y: bounds.minY)
        
        let sortedViews = subviews.sorted { v1, v2 in
            v1[PreferredPosition.self] > v2[PreferredPosition.self]
        }
        
        for v in sortedViews {
            v.place(at: pt, anchor: .topLeading, proposal: .unspecified)
            
            pt.x += v.sizeThatFits(.unspecified).width + spacing
        }
    }
}

這段代碼不像第一段我們寫的 layoutPriority 那樣整潔,但是用這兩個擴(kuò)展很容易解決:

extension View {
    func preferredPosition(_ order: CGFloat) -> some View {
        self.layoutValue(key: PreferredPosition.self, value: order)
    }
}

extension LayoutSubview {
    var preferredPosition: CGFloat {
        self[PreferredPosition.self]
    }
}

現(xiàn)在我們可以像這樣重寫:

SimpleHStack(spacing: 5) {
    Circle().fill(.yellow)
         .frame(width: 30, height: 30)
                        
    Circle().fill(.green)
        .frame(width: 30, height: 30)

    Circle().fill(.blue)
        .frame(width: 30, height: 30)
        .preferredPosition(1)
}
struct SimpleHStack: Layout {
    // ...

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) 
    {
        var pt = CGPoint(x: bounds.minX, y: bounds.minY)
        
        for v in subviews.sorted(by: { $0.preferredPosition > $1.preferredPosition }) {
            v.place(at: pt, anchor: .topLeading, proposal: .unspecified)
            
            pt.x += v.sizeThatFits(.unspecified).width + spacing
        }
    }

}

默認(rèn)間距

到目前為止,我們在初始化布局的時候 SimpleHStack 使用的都是我們提供的間距值,然而,在你使用了 HStack 一陣子,你就會知道如果沒有指明間距,視圖將會根據(jù)不同的平臺和內(nèi)容提供默認(rèn)的間距。一個視圖可以擁有不同間距,如果旁邊是文本視圖和旁邊是圖像間距是不一樣的。除此之外,每個邊緣都會有自己的偏好。

所以我們應(yīng)該如何用 SimpleHStack 讓它們行為一致?我曾提到過子視圖代理是布局知識的寶藏,而且它們不會讓人失望。它們有可以查詢它們空間偏好的方法。

struct SimpleHStack: Layout {
    
    var spacing: CGFloat? = nil
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let idealViewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
        let accumulatedWidths = idealViewSizes.reduce(0) { $0 + $1.width }
        let maxHeight = idealViewSizes.reduce(0) { max($0, $1.height) }

        let spaces = computeSpaces(subviews: subviews)
        let accumulatedSpaces = spaces.reduce(0) { $0 + $1 }
        
        return CGSize(width: accumulatedSpaces + accumulatedWidths,
                      height: maxHeight)
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) 
    {
        var pt = CGPoint(x: bounds.minX, y: bounds.minY)
        let spaces = computeSpaces(subviews: subviews)

        for idx in subviews.indices {
            subviews[idx].place(at: pt, anchor: .topLeading, proposal: .unspecified)
            
            if idx < subviews.count - 1 {
                pt.x += subviews[idx].sizeThatFits(.unspecified).width + spaces[idx]
            }
        }
    }
    
    func computeSpaces(subviews: LayoutSubviews) -> [CGFloat] {
        if let spacing {
            return Array<CGFloat>(repeating: spacing, count: subviews.count - 1)
        } else {
            return subviews.indices.map { idx in
                guard idx < subviews.count - 1 else { return CGFloat(0) }
                
                return subviews[idx].spacing.distance(to: subviews[idx+1].spacing, along: .horizontal)
            }
        }

    }
}

請注意,除了使用空間偏好外,你還可以告訴系統(tǒng)容器視圖的空間偏好。這樣, SwiftUI 就會知道如何將其與周圍的視圖分開,為此,你需要實(shí)現(xiàn)布局方法 spacing(subviews:cache:)。

布局屬性和 Spacer()

布局協(xié)議有一個你可以實(shí)現(xiàn)的名為 layoutProperties 的靜態(tài)屬性。根據(jù)文檔, LayoutProperties 包含布局容器的特定布局屬性。在寫這篇文章時,只定義了一個屬性:stackOrientation

struct MyLayout: Layout {
    
    static var layoutProperties: LayoutProperties {
        var properties = LayoutProperties()
        
        properties.stackOrientation = .vertical
        
        return properties
    }

    // ...
}

stackOrientation 告訴是像 Spacer 這樣的視圖是否應(yīng)該在橫軸或縱軸上展開。例如,如果你檢查 Spacer 視圖代理的最小,理想和最大尺寸,這就是它在不同容器返回的結(jié)果,每個容器都有不同的stackOrientation

stackOrientation minimum ideal maximum
.horizontal 8.0 × 0.0 8.0 × 0.0 .infinity × 0.0
.vertical 0.0 × 8.0 0.0 × 8.0 0.0 × .infinity
.none or nil 8.0 × 8.0 8.0 × 8.0 .infinity × .infinity

布局緩存

布局緩存是常被用來提高我們布局性能的一種方式。然而,它還有別的用途。只需要把它看作是一個存儲數(shù)據(jù)的地方,我們需要在 sizeThatFitsplaceSubviews 調(diào)用中持久保存。首先想到的是提高性能,但是,它對于和其他子視圖布局共享信息也是非常有用的。當(dāng)我們講到組合布局的例子時,我們將對此進(jìn)行探討,但讓我們從了解如何使用緩存提高性能開始。

SwiftUI 的布局過程中會多次調(diào)用 sizeThatFitsplaceSubviews 方法。這個框架測試我們的容器的靈活性,以確定整體視圖層級結(jié)構(gòu)的最終布局。為了提高布局容器性能, SwiftUI 讓我們實(shí)現(xiàn)了一個緩存, 只有當(dāng)容器內(nèi)的至少一個視圖改變時才更新緩存。因?yàn)?sizeThatFitsplaceSubviews 都可以為單個視圖更改時多次調(diào)用,所以保留不需要為每次調(diào)用而重新計(jì)算的數(shù)據(jù)緩存是有意義的。

使用緩存不是必須的。事實(shí)上,很多時候你不需要。無論如何,在沒有緩存的情況下編寫我們的布局更簡單一點(diǎn),當(dāng)我們以后需要時再添加。 SwiftUI 已經(jīng)做了一些緩存。例如,從子視圖代理獲得的值會自動存儲在緩存中。相同的參數(shù)的反復(fù)調(diào)用將會使用緩存結(jié)果。在 makeCache(subviews:) 文檔頁面,有一個很好的討論關(guān)于你可能想要實(shí)現(xiàn)自己的緩存的原因。

同時也要注意, sizeThatFitsplaceSubviews 中的緩存參數(shù)有一個是 inout 參數(shù),這意味著你也可以用這個函數(shù)更新緩存存儲,我們將會看到它在 RecursiveWheel 例子中特別有幫助。

例如,這里是使用更新緩存的 SimpleHStack 。下面是我們需要做的:

  • 創(chuàng)建一個將包含緩存數(shù)據(jù)的類型。在本例中,我把它叫做 CacheData ,它將會計(jì)算視圖間的最大高度和空間。
  • 實(shí)現(xiàn) makeCache(subviews:) 創(chuàng)建緩存。
  • 可選的實(shí)現(xiàn) updateCache(subviews:),這個方法會在檢測到更改時調(diào)用。它提供了默認(rèn)實(shí)現(xiàn),基本上通過調(diào)用 makeCache 重新創(chuàng)建緩存。
  • 記住要更新 sizeThatFitsplaceSubviews中的緩存參數(shù)類型。
struct SimpleHStack: Layout {
    struct CacheData {
        var maxHeight: CGFloat
        var spaces: [CGFloat]
    }
    
    var spacing: CGFloat? = nil
    
    func makeCache(subviews: Subviews) -> CacheData {
        return CacheData(maxHeight: computeMaxHeight(subviews: subviews),
                         spaces: computeSpaces(subviews: subviews))
    }
    
    func updateCache(_ cache: inout CacheData, subviews: Subviews) {
        cache.maxHeight = computeMaxHeight(subviews: subviews)
        cache.spaces = computeSpaces(subviews: subviews)
    }
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) -> CGSize {
        let idealViewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
        let accumulatedWidths = idealViewSizes.reduce(0) { $0 + $1.width }
        let accumulatedSpaces = cache.spaces.reduce(0) { $0 + $1 }
        
        return CGSize(width: accumulatedSpaces + accumulatedWidths,
                      height: cache.maxHeight)
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) {
        var pt = CGPoint(x: bounds.minX, y: bounds.minY)

        for idx in subviews.indices {
            subviews[idx].place(at: pt, anchor: .topLeading, proposal: .unspecified)
            
            if idx < subviews.count - 1 {
                pt.x += subviews[idx].sizeThatFits(.unspecified).width + cache.spaces[idx]
            }
        }
    }
    
    func computeSpaces(subviews: LayoutSubviews) -> [CGFloat] {
        if let spacing {
            return Array<CGFloat>(repeating: spacing, count: subviews.count - 1)
        } else {
            return subviews.indices.map { idx in
                guard idx < subviews.count - 1 else { return CGFloat(0) }
                
                return subviews[idx].spacing.distance(to: subviews[idx+1].spacing, along: .horizontal)
            }
        }
    }
    
    func computeMaxHeight(subviews: LayoutSubviews) -> CGFloat {
        return subviews.map { $0.sizeThatFits(.unspecified) }.reduce(0) { max($0, $1.height) }
    }
}

如果我們每次調(diào)用其中一個布局函數(shù)都打印出一條信息,我們將會獲得的下面的結(jié)果。如你所見,緩存將會計(jì)算兩次,但是其他方法將會被調(diào)用25次!

makeCache called <<<<<<<<
sizeThatFits called
sizeThatFits called
sizeThatFits called
sizeThatFits called
placeSubiews called
placeSubiews called
updateCache called <<<<<<<<
sizeThatFits called
sizeThatFits called
sizeThatFits called
sizeThatFits called
placeSubiews called
placeSubiews called
sizeThatFits called
sizeThatFits called
placeSubiews called
sizeThatFits called
placeSubiews called
placeSubiews called
sizeThatFits called
placeSubiews called
placeSubiews called
sizeThatFits called
sizeThatFits called
sizeThatFits called
placeSubiews called

注意除了使用緩存參數(shù)提高性能,它們也有其他用途。我們將會在第二部分的 RecursiveWheel例子中再談?wù)摗?/p>

高明的偽裝者

正如我已經(jīng)提到的,布局協(xié)議沒有采用視圖協(xié)議。那么我們?yōu)槭裁匆恢痹?ViewBuilder中使用布局容器,就好像它們是視圖一樣?事實(shí)證明,當(dāng)你用代碼放置你的布局時,會有一個系統(tǒng)函數(shù)調(diào)用來產(chǎn)生視圖。那這個函數(shù)叫什么呢?你可能已經(jīng)猜到了:

func callAsFunction<V>(@ViewBuilder _ content: () -> V) -> some View where V : View

由于語言的增加(在 SE-0253中有描述和解釋),被命名為 callAsFunction 的方法是特殊的。當(dāng)我們使用一個類型實(shí)例時,這些方法會像一個函數(shù)一樣被調(diào)用。在這種情況下,我們可能會感到困惑,因?yàn)槲覀兯坪踔皇窃诔跏蓟愋停鴮?shí)際上,我們做的更多。我們初始化類型然后調(diào)用 callAsFunction,因?yàn)?callAsFunction的返回值是一個視圖,所以我們可以把它放到我們的 SwiftUI 代碼中。

SimpleHStack(spacing: 10).callAsFunction({
    Text("Hello World!")
})

// Thanks to SE-0253 we can abbreviate it by removing the .callAsFunction
SimpleHStack(spacing: 10)({
    Text("Hello World!")
})

// And thanks to trailing closures, we end up with:
SimpleHStack(spacing: 10) {
    Text("Hello World!")
}

如果布局沒有初始化參數(shù),代碼甚至可以更簡單:

SimpleHStack().callAsFunction({
    Text("Hello World!")
})

// Thanks to SE-0253 we can abbreviate it by removing the .callAsFunction
SimpleHStack()({
    Text("Hello World!")
})

// And thanks to single trailing closures, we end up with:
SimpleHStack {
    Text("Hello World!")
}

所以你明白了,布局類型并不是視圖,但是當(dāng)你在 SwiftUI 中使用它們的時候它們就會產(chǎn)生一個視圖。這個技巧(callAsFunction)還可以切換到不同布局,同時保持視圖的標(biāo)識,就像接下來的部分描述的那樣。

使用 AnyLayout 切換布局

布局容器的另一個有趣的地方,我們可以修改容器的布局, SwiftUI 會友好地用動畫處理兩者的切換。不需要額外的代碼!那是因?yàn)橐晥D會識別標(biāo)識并且維護(hù), SwiftUI 將這個行為認(rèn)為是視圖的改變,而不是兩個單獨(dú)的視圖。

SwiftUI 布局協(xié)議 - Part1

struct ContentView: View {
    @State var isVertical = false
    
    var body: some View {
        let layout = isVertical ? AnyLayout(VStackLayout(spacing: 5)) : AnyLayout(HStackLayout(spacing: 10))
        
        layout {
            Group {
                Image(systemName: "globe")
                
                Text("Hello World!")
            }
            .font(.largeTitle)
        }
        
        Button("Toggle Stack") {
            withAnimation(.easeInOut(duration: 1.0)) {                
                isVertical.toggle()
            }
        }
    }
}

三元運(yùn)算符(條件?結(jié)果1:結(jié)果2)要求兩個表達(dá)式返回同一類型。AnyLayout 在這里發(fā)揮了作用。

注意:如果你觀看過 2022 WWDC Layout session,你或許看見過蘋果工程師使用的例子,但使用的是 VStack 代替 VStackLayoutHStack 代替 HStackLayout 。那已經(jīng)過時了。在 beta3 過后, HStackVStack 不再采用布局協(xié)議,并且他們添加了 VStackLayoutHStackLayout 布局(分別由HStackVStack 使用),他們還添加了 ZStackLayoutGridLayout

結(jié)語

如果我們停下來考慮每一種可能的情況,編寫布局容器可能會讓我們舉步維艱。有的視圖使用盡可能多的空間,有的視圖會盡量適應(yīng),還有的將會使用的更少,等等。當(dāng)然還有布局優(yōu)先級,當(dāng)多個視圖需要競爭同一個空間會變得更加艱難。然而,這項(xiàng)任務(wù)可能并不像看起來艱巨。

我們可能會使用自己的布局,并且可能會提前知道我們的容器會有什么類型的視圖。例如,如果你打算只用方形圖片或者文本視圖來使用自己的容器,或者你知道你的容器會有具體尺寸,或者你確定你所有的視圖都擁有一樣的優(yōu)先級,等等。這些信息都可以大大的簡化任務(wù)。即使你不能有這種奢望來做這種假設(shè),它也可能是開始編碼的好地方,讓你的布局在一些情況下工作,然后開始為更復(fù)雜的情況添加代碼。文章來源地址http://www.zghlxwxcb.cn/news/detail-454482.html

到了這里,關(guān)于SwiftUI 布局協(xié)議 - Part1的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 計(jì)算機(jī)網(wǎng)絡(luò) Part1

    目錄 計(jì)算機(jī)網(wǎng)絡(luò)的一些術(shù)語: 計(jì)算機(jī)網(wǎng)絡(luò)的重點(diǎn): 整體結(jié)構(gòu)以及HTTP部分一些問題: TCP以及UDP相關(guān)的問題: DNS以及網(wǎng)絡(luò)安全相關(guān)的問題: 1. OSI七層模型的每一層分別是什么?對應(yīng)的協(xié)議有哪些? 傳輸層協(xié)議和網(wǎng)絡(luò)層協(xié)議有什么區(qū)別? ? HTTP協(xié)議為什么設(shè)計(jì)為無狀態(tài)的? H

    2024年02月12日
    瀏覽(23)
  • Leetcode with Golang 滑動窗口 Part1

    Leetcode with Golang 滑動窗口 Part1

    滑動窗口的定義: 滑動窗口這一個技巧主要運(yùn)用于處理數(shù)組問題上,一般用于“子串”問題。精髓是,維護(hù)一個里面裝著元素的“窗口”,在將新元素裝進(jìn)“窗口”的同時,根據(jù)題意,把不符合題意的元素踢出“窗口”。 滑動窗口的模板: 接下來看幾道題目: Leetcode 209.長

    2024年01月19日
    瀏覽(23)
  • 當(dāng)代軟件工程師技術(shù)面試準(zhǔn)備Part1

    當(dāng)代軟件工程師技術(shù)面試準(zhǔn)備Part1

    當(dāng)代軟件工程師技術(shù)面試準(zhǔn)備Part1 一. 編碼 - Leetcode ??? LeetCode 是一個在線的編程練習(xí)平臺,專注于幫助程序員提升他們的編程技能。該平臺提供了大量的算法和數(shù)據(jù)結(jié)構(gòu)問題,涵蓋了各種難度級別,從簡單到困難。LeetCode的主要目標(biāo)是幫助程序員準(zhǔn)備技術(shù)面試,特別是在軟

    2024年02月03日
    瀏覽(22)
  • Python八股文:基礎(chǔ)知識Part1

    Python八股文:基礎(chǔ)知識Part1

    1. 不用中間變量交換 a 和 b 這是python非常方便的一個功能可以這樣直接交換兩個值? 2. 可變數(shù)據(jù)類型字典在for 循環(huán)中進(jìn)行修改 這道題在這里就是讓我們?nèi)セ卮疠敵龅膬?nèi)容,這里看似我們是在for循環(huán)中每一次加入了都在list中加入了一個字典,然后字典的鍵值對的value每次都加

    2024年04月12日
    瀏覽(14)
  • C/C++文件操作(細(xì)節(jié)滿滿,part1)

    C/C++文件操作(細(xì)節(jié)滿滿,part1)

    個人主頁: 仍有未知等待探索_C語言疑難,數(shù)據(jù)結(jié)構(gòu),PTA-CSDN博客 專題分欄: C語言疑難_仍有未知等待探索的博客-CSDN博客 目錄 一、引言? 二、什么是文件? 1、程序文件 2、數(shù)據(jù)文件 3、文件名? 4、文件路徑? 1.相對路徑 2.絕對路徑 三、文件的打開和關(guān)閉? 1、文件操作的大體流

    2024年02月08日
    瀏覽(25)
  • NzN的數(shù)據(jù)結(jié)構(gòu)--二叉樹part1

    NzN的數(shù)據(jù)結(jié)構(gòu)--二叉樹part1

    ? ? ? ? 你叉叉,讓你學(xué)數(shù)據(jù)結(jié)構(gòu)你不學(xué);你叉叉,讓你看二叉樹你不看。?今天我們來一起學(xué)習(xí)二叉樹部分, 先贊后看是好習(xí)慣 。 ? ????????樹是一種 非線性 的數(shù)據(jù)結(jié)構(gòu),它是由n(n=0)個有限結(jié)點(diǎn)組成一個具有層次關(guān)系的集合。把它叫做樹是因?yàn)樗雌饋硐褚豢玫箳?/p>

    2024年04月13日
    瀏覽(32)
  • 使用go語言構(gòu)建區(qū)塊鏈 Part1.基礎(chǔ)原型

    英文源地址 區(qū)塊鏈技術(shù)是21世紀(jì)最具變革型的技術(shù)之一,它仍處于成長階段, 其潛力尚未完全實(shí)現(xiàn).從本質(zhì)上說, 區(qū)塊鏈?zhǔn)且粋€分布式的記賬數(shù)據(jù)庫.但它的獨(dú)特之處在于它不是一個私有數(shù)據(jù)庫,而是一個公共數(shù)據(jù)庫, 也就是說, 每個使用它的人都有它的完整或部分副本.而且,只有在

    2024年02月07日
    瀏覽(17)
  • python文件處理之open()功能的使用part1

    什么是文件? 文件是操作系統(tǒng)提供給用戶或者是應(yīng)用程序用于操作硬盤的虛擬概念或者說接口。 為什么要有文件? 用戶通過應(yīng)用程序可以通過文件,將數(shù)據(jù)永久保存到硬盤中。 詳細(xì)的說:用戶和應(yīng)用程序操作的是文件,對文件的所有操作,都是向操作系統(tǒng)發(fā)送系統(tǒng)調(diào)用,然

    2023年04月08日
    瀏覽(23)
  • Elasticsearch8.4.0集群安裝(ELK安裝part1)

    Elasticsearch8.4.0集群安裝(ELK安裝part1)

    一,環(huán)境準(zhǔn)備 由于資源有限,使用VirtulBox虛擬機(jī)進(jìn)行搭建。 搭建集群的環(huán)境配置: 本集群使用Red Hat Enterprise Linux release 8.2 (Ootpa)操作系統(tǒng),1C CPU,4G Memory,大于50G的Disk。 集群安裝規(guī)劃如下: 機(jī)器地址?? ? 節(jié)點(diǎn)名稱?? ?節(jié)點(diǎn)角色?? ?節(jié)點(diǎn)功能 192.168.88.5 node-1?? ?Master,da

    2023年04月24日
    瀏覽(21)
  • 第七章:敏捷開發(fā)工具方法-part1-敏捷開發(fā)基礎(chǔ)

    第七章:敏捷開發(fā)工具方法-part1-敏捷開發(fā)基礎(chǔ)

    敏捷開發(fā)背景 速度是企業(yè)競爭致勝的關(guān)鍵因素,軟件項(xiàng)目的最大挑戰(zhàn)在于一方面需要應(yīng)付變動中的需求,一方面需要在有限的時間完成項(xiàng)目,傳統(tǒng)的軟件工程難以滿足這些要求 所以軟件團(tuán)隊(duì)除了在技術(shù)上必須日益精進(jìn),更需要運(yùn)用有效的開發(fā)流程,以確保團(tuán)隊(duì)能夠發(fā)揮綜效

    2024年02月09日
    瀏覽(44)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包