隨著本人對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ā)出這樣一句驚嘆:“原來如此M衅簟!攘宙!”
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一樣的:
我們在此基礎(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
我們看下布局的效果:
這里大家必須要理解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))
}
}
}
可以看出诊胞,黃色方塊的寬度依賴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的時候,他們遵循下邊這個流程:
其實攒至,如果大家理解了布局的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:
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:
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ù)后耿戚,就超過了一行的顯示范圍了!0⒊谩膜蛔!")
.border(Color.blue)
.frame(width: 200, height: 100)
.border(Color.green)
.font(.title)
}
}
按照3大布局原則,綠色邊框的寬為200脖阵, 高為100皂股, 藍色邊框的父view提供的寬為200, 高為100命黔,其child呜呐, text在寬為200, 高為100限制下悍募,返回了籃框的size蘑辑,因此籃框和text的size相同。這個結(jié)果符合我們分析的結(jié)果搜立。
我們修改一下代碼:
struct ContentView: View {
var body: some View {
Text("這個文本還挺長的以躯,到達了一定字數(shù)后,就超過了一行的顯示范圍了W挠弧S巧琛!")
.fixedSize(horizontal: true, vertical: false)
.border(Color.blue)
.frame(width: 200, height: 100)
.border(Color.green)
.font(.title)
}
}
可以看到颠通,綠框沒有任何變化址晕,籃框變寬了,當(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ù)后匙隔,就超過了一行的顯示范圍了R缮弧!纷责!")
.frame(idealWidth: 300)
.fixedSize(horizontal: true, vertical: false)
.border(Color.blue)
.frame(width: 200, height: 100)
.border(Color.green)
.font(.title)
}
}
可以看出來捍掺,完全符合我們預(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))
}
}
運行效果如下:
上邊的代碼其實很簡單不瓶,如果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)
}
}
可以看出來,這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)
}
}
可以明顯的看出來秀仲,優(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),立即刪除炮障。