SwiftUI之frame詳解

隨著本人對SwiftUI了解地越來越深入,我發(fā)現(xiàn)SwiftUI并不像表面上看上去的那么簡單,在初學(xué)的時候,我們看到的東西往往是浮在水面上最直觀的表象,隨著我們的下潛,我們就看到了那些有趣深奧,充滿魅力的東西。也許,之前我們認為用SwiftUI比較難實現(xiàn)的功能,此時此刻,卻變得十分easy。

對于frame來說,很多人覺得它實在是太簡單了,做過iOS開發(fā)的都知道frame是怎么一回事,bounds是怎么一回事,但在SwiftUI中,它幾乎完全不同于我們平時用過的frame。SwiftUI本質(zhì)上運行在一套新的規(guī)則之上,對于SwiftUI來說,frame當(dāng)然也有它自己的規(guī)則。

在原作者的文章中,他并沒有講解SwiftUI中布局的基本原則, 對于部分讀者來說,理解原文可能會有一點困難,在本篇文章中,我會用一部分的篇幅,來講解SwiftUI中布局的基本原則,結(jié)合這些原則,再回頭去看frame,一定會發(fā)出這樣一句驚嘆:“原來如此?。?!”

frame是什么

在SwiftUI中,frame()是一個modifier,modifier在SwiftUI中并不是真的修改了view。大多數(shù)情況下,當(dāng)我們對某個view應(yīng)用一個modifier的時候,實際上會創(chuàng)建一個新的view。

在SwiftUI中,views并沒有frame的概念,但是它們有bounds的概念,也就是說每個view都有一個范圍和大小,它們的bounds不能夠直接通過手動的方式去修改。

當(dāng)某個view的frame改變后,其child的size不一定會變化,比如,我們修改一個容器VStack的寬度后,其內(nèi)部child的布局有可能變化,也有可能不變化。我們會在下邊驗證這個說法。

大家記住這句話,每個view對自己需要的size,都有自己的想法,這是我們下邊內(nèi)容講解的核心思想。

Behaviors

在SwfitUI中,view在計算自己size的時候會有不同的行為方式,我們分為4類:

  • 類似于Vstack,它們會盡可能讓自己內(nèi)部的內(nèi)容展示完整,但也不會多要其他的額外空間
  • 類似于Text這種只返回自身需要的size,如果size不夠,它非常聰明的做一些額外的操作,比如換行等等
  • 類似于Shape這種給多大尺寸就使用多大尺寸
  • 還有一些可能超出父控件的view

還存在其他一些比較特殊的例外,比如Spacer,他的特性跟他屬于哪個容器或者哪個軸有關(guān)系。當(dāng)他在VStack中時,他會盡可能的占據(jù)剩余垂直的全部空間,而占據(jù)的水平空間為0,在HStack中,他的行為卻又恰恰相反。

我們在下一小節(jié)的布局原則中,就會看到這些不同行為的表現(xiàn)了。

布局原則

大家仔細思考我接下來的這3句話:

  • 當(dāng)布局某個view時,其父view會給出一個建議的size
  • 如果該view存在child,那么就拿著這個建議的尺寸去問他的child,child根據(jù)自身的behavior返回一個size,如果沒有child,則根據(jù)自身的behavior返回一個size
  • 用該size在其父view中進行布局

我們看一個簡單的例子:

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
    }
}

布局的過程是自下而上的,我們計算ContentView的size

  • ContentView的父view為其提供了一個size等于全屏幕的建議尺寸
  • ContentVIew拿著該尺寸去問其child,Text返回了一個自身需要的size
  • 用該size在父view中布局

基于這3個基本原則,我們分析出,ContentView的size其實是跟Text一樣的:

1

我們在此基礎(chǔ)上再增加一點難度:

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .frame(width: 200, height: 100)
            .background(Color.green)
            .frame(width: 400, height: 200)
            .background(Color.orange.opacity(0.5))
    }
}

上邊這段代碼基本上能夠代表任何一個自定義view的情況了,不要忘記,在考慮布局的時候,是自下而上的。

我們先考慮ContentVIew,他的父view給他的建議尺寸為整個屏幕的大小,我們稱為size0,他去詢問他的child,他的child為最下邊的那個background,這個background自己也不知道自己的size,因此他繼續(xù)拿著size0去詢問他自己的child,他的child是個frame,返回了width400, height200, 因此background告訴ContentView他需要的size為width400, height200,因此最終ContentView的size為width400, height200。

很顯然,我們也計算出了最下邊background的size,注意,里邊的Color也是一個view,Color本身是一個Shape,background返回一個透明的view

我們再考慮最上邊的background,他父view給的建議的size為width: 400, height: 200,他詢問其child,得到了需要的size為width: 200, height: 100,因此該background的size為width: 200, height: 100。

我們在看Text,父View給的建議的size為width: 200, height: 100,但其只需要正好容納文本的size,因此他的size并不會是width: 200, height: 100

我們看下布局的效果:

2

這里大家必須要理解Text的size并不會是width: 200, height: 100,這跟我們平時開發(fā)的思維有所不同。

了解了這些布局的知識后,我們再往下看文章,就不會有那么的疑惑,在平時的開發(fā)中,對于出現(xiàn)比較奇怪的布局問題,也能知道造成這些問題的原因是什么了。

基本用法

我們在開發(fā)中,使用frame最頻繁的方法是:

func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center)

我們之前寫了一篇專門講解alignment的文章;SwiftUI之AlignmentGuides,沒有看過的同學(xué)一定要去看一下, 在SwiftUI中,理解Alignment Guides的用法,能夠讓我們開發(fā)效果更加高效。

當(dāng)我們修改了width或者height的時候,大多數(shù)情況下布局的效果跟我們想象中的一樣,表面上看,我們通過這個方法能夠設(shè)置width和height,實際上frame本質(zhì)上并不能直接修改view的size。

我們在上一小節(jié),演示了布局的3個步驟,frame恰恰能夠改變父或者子的size值,當(dāng)view詢問child的時候,如果遇到frame,則直接使用該size作為child返回的size。

接下來我們演示一個小demo, 當(dāng)我們修改父view的寬度的時候,子view不一定完全隨著父view的寬度改變而改變。大家將會看到,布局的3個步驟再次驗證了這些變化。

struct ExampleView: View {
    @State private var width: CGFloat = 50
    
    var body: some View {
        VStack {
            SubView()
                .frame(width: self.width, height: 120)
                .border(Color.blue, width: 2)
            
            Text("Offered Width \(Int(width))")
            Slider(value: $width, in: 0...200, step: 1)
        }
    }
}


struct SubView: View {
    var body: some View {
        GeometryReader { proxy in
            Rectangle()
                .fill(Color.yellow.opacity(0.7))
                .frame(width: max(proxy.size.width, 120), height: max(proxy.size.height, 120))
        }
    }
}
3

可以看出,黃色方塊的寬度依賴frame(width: max(proxy.size.width, 120), height: max(proxy.size.height, 120)),他在計算size的時候,會使用該frame限定的size,因此,上邊顯示的效果正好符合我們的預(yù)期。

其他用法

出了上邊的基本用法外,還有下邊這樣的用法:

func frame(minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, maxWidth: CGFloat? = nil, minHeight: CGFloat? = nil, idealHeight: CGFloat? = nil, maxHeight: CGFloat? = nil, alignment: Alignment = .center)

很明顯,這么多參數(shù)可以分為3組:

  • minWidth,idealWidth,maxWidth
  • minHeight,idealHeight,maxHeight
  • alignment

最后一組我們在其他文章中已經(jīng)講的很明白了,第一組和第二組在原理上基本相同,我們重點拿出第一組來做一個詳細的講解。

當(dāng)我們給minWidth,idealWidth,maxWidth賦值的時候,一定要遵循數(shù)值遞增原則,否則,xcode會給出錯誤提示。

minWidth表示的是最小的寬度, idealWidth表示最合適的寬度,maxWidth表示最大的寬度,通常如果我們用到了該方法,我們只需要考慮minWidth和maxWidth就行了。

在計算size的時候,他們遵循下邊這個流程:

4

其實,如果大家理解了布局的3個原則,那么理解這個流程就很簡單了,frame modifier通過計算minWidth,maxWidth和child size ,就可以看著上邊的規(guī)則返回一個size,view用這個size作為自身在父view中的size。

我們簡單看幾個例子:

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .frame(minWidth: 40, maxWidth: 400)
            .background(Color.orange.opacity(0.5))
            .font(.largeTitle)
    }
}

上邊的代碼中,我們同時設(shè)置了minWidth和maxWidth,background的size返回400:

5
struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .frame(minWidth: 400)
            .background(Color.orange.opacity(0.5))
            .font(.largeTitle)
    }
}

如果只設(shè)置了minWidth,那么background的size返回400:

6
struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .frame(maxWidth: 400)
            .background(Color.orange.opacity(0.5))
            .font(.largeTitle)
    }
}

只要設(shè)置了maxWidth,background返回的就是maxWidth的值。

關(guān)于這里流程的各種各樣的情況,大家只需要自己寫一點代碼實驗一下就行了,總之,按照前邊說的布局3大原則來理解布局就行了。

Fixed Size Views

我們一定見過 .fixedSize()`這個modifier,表面上看,他好像應(yīng)該是用在Text上的,用來固定Text的寬度,相信很多同學(xué)應(yīng)該是這個想法,在這一小節(jié),我們就會徹底理解它究竟是怎樣一個東西。

func fixedSize() -> some View
func fixedSize(horizontal: Bool, vertical: Bool) -> some View

在SwiftUI中,任何View都可以用這個modifer,當(dāng)我們應(yīng)用了該modifier后,布局系統(tǒng)在返回size的時候,就會返回與之對應(yīng)的idealWIdth或者idealHeight。

我們先看一段代碼:

struct ContentView: View {
    var body: some View {
        Text("這個文本還挺長的,到達了一定字數(shù)后,就超過了一行的顯示范圍了?。。?)
            .border(Color.blue)
            .frame(width: 200, height: 100)
            .border(Color.green)
            .font(.title)
    }
}
7

按照3大布局原則,綠色邊框的寬為200, 高為100, 藍色邊框的父view提供的寬為200, 高為100,其child, text在寬為200, 高為100限制下,返回了籃框的size,因此籃框和text的size相同。這個結(jié)果符合我們分析的結(jié)果。

我們修改一下代碼:

struct ContentView: View {
    var body: some View {
        Text("這個文本還挺長的,到達了一定字數(shù)后,就超過了一行的顯示范圍了?。?!")
            .fixedSize(horizontal: true, vertical: false)
            .border(Color.blue)
            .frame(width: 200, height: 100)
            .border(Color.green)
            .font(.title)
    }
}
8

可以看到,綠框沒有任何變化,籃框變寬了,當(dāng)在水平方向上應(yīng)用了fixedSize時,.border(Color.blue)在詢問child的size時,child會返回它的idealWidth,我們并沒有給出一個指定的idealWidth,每個view里邊都有自己的idealWidth。

那么我們驗證下,我們給它顯式的指定一個idealWidth:

struct ContentView: View {
    var body: some View {
        Text("這個文本還挺長的,到達了一定字數(shù)后,就超過了一行的顯示范圍了!??!")
            .frame(idealWidth: 300)
            .fixedSize(horizontal: true, vertical: false)
            .border(Color.blue)
            .frame(width: 200, height: 100)
            .border(Color.green)
            .font(.title)
    }
}
9

可以看出來,完全符合我們預(yù)想的結(jié)果,因此,當(dāng)我們想要固定某個view的某個軸的尺寸的時候,fixedSize這個modifier是一個利器。

應(yīng)用

原作者寫了一個演示fixedSize的小demo,下邊是完整代碼:

struct ExampleView: View {
    @State private var width: CGFloat = 150
    @State private var fixedSize: Bool = true
    
    var body: some View {
        GeometryReader { proxy in
            
            VStack {
                Spacer()
                
                VStack {
                    LittleSquares(total: 7)
                        .border(Color.green)
                        .fixedSize(horizontal: self.fixedSize, vertical: false)
                }
                .frame(width: self.width)
                .border(Color.primary)
                .background(MyGradient())
                
                Spacer()
                
                Form {
                    Slider(value: self.$width, in: 0...proxy.size.width)
                    Toggle(isOn: self.$fixedSize) { Text("Fixed Width") }
                }
            }
        }.padding(.top, 140)
    }
}

struct LittleSquares: View {
    let sqSize: CGFloat = 20
    let total: Int
    
    var body: some View {
        GeometryReader { proxy in
            HStack(spacing: 5) {
                ForEach(0..<self.maxSquares(proxy), id: \.self) { _ in
                    RoundedRectangle(cornerRadius: 5).frame(width: self.sqSize, height: self.sqSize)
                        .foregroundColor(self.allFit(proxy) ? .green : .red)
                }
            }
        }.frame(idealWidth: (5 + self.sqSize) * CGFloat(self.total), maxWidth: (5 + self.sqSize) * CGFloat(self.total))
    }

    func maxSquares(_ proxy: GeometryProxy) -> Int {
        return min(Int(proxy.size.width / (sqSize + 5)), total)
    }
    
    func allFit(_ proxy: GeometryProxy) -> Bool {
        return maxSquares(proxy) == total
    }
}

struct MyGradient: View {
    var body: some View {
        LinearGradient(gradient: Gradient(colors: [Color.red.opacity(0.1), Color.green.opacity(0.1)]), startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 1))
    }
}

運行效果如下:

10

上邊的代碼其實很簡單,如果idealWidth來固定住view的寬度,那么view的寬度就不會改變,這在某些場景下還是挺有用的。

上邊例子中最核心的代碼是:

.frame(idealWidth: (5 + self.sqSize) * CGFloat(self.total), maxWidth: (5 + self.sqSize) * CGFloat(self.total))

Layout Priority

SwiftUI中,view默認的layout priority 都是0,對于同一層級的view來說,系統(tǒng)會按照順序進行布局,當(dāng)我們使用.layourPriority()修改了布局的優(yōu)先級后,系統(tǒng)則優(yōu)先布局高優(yōu)先級的view。

struct ContentView: View {
    var body: some View {
        VStack {
            Text("床前明月光,疑是地上霜")
                .background(Color.green)
            Text("舉頭望明月,低頭思故鄉(xiāng)")
                .background(Color.blue)
        }
        .frame(width: 100, height: 100)
    }
}
11

可以看出來,這2個text的優(yōu)先級是相同的,因此他們平分布局空間,我們給第2個text提升一點優(yōu)先級:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("床前明月光,疑是地上霜")
                .background(Color.green)
            Text("舉頭望明月,低頭思故鄉(xiāng)")
                .background(Color.blue)
                .layoutPriority(1)
        }
        .frame(width: 100, height: 100)
    }
}

12

可以明顯的看出來,優(yōu)先布局第2個text。符合我們的預(yù)期。

總結(jié)

這篇文章中,講解了frame的用法,fixedSize和layoutPriority的用法,要想理解這些用法,必須理解布局的3大原則:

  • 父view提供一個建議的size
  • view根據(jù)自身的特點再結(jié)合它的child計算出一個size
  • 使用該size在父view中布局

*注:上邊的內(nèi)容參考了網(wǎng)站https://swiftui-lab.com/frame-behaviors/,如有侵權(quán),立即刪除。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容