隨著本人對SwiftUI了解地越來越深入,我發現SwiftUI并不像表面上看上去的那么簡單,在初學的時候,我們看到的東西往往是浮在水面上最直觀的表象,隨著我們的下潛,我們就看到了那些有趣深奧,充滿魅力的東西。也許,之前我們認為用SwiftUI比較難實現的功能,此時此刻,卻變得十分easy。
對于frame來說,很多人覺得它實在是太簡單了,做過iOS開發的都知道frame是怎么一回事,bounds是怎么一回事,但在SwiftUI中,它幾乎完全不同于我們平時用過的frame。SwiftUI本質上運行在一套新的規則之上,對于SwiftUI來說,frame當然也有它自己的規則。
在原作者的文章中,他并沒有講解SwiftUI中布局的基本原則, 對于部分讀者來說,理解原文可能會有一點困難,在本篇文章中,我會用一部分的篇幅,來講解SwiftUI中布局的基本原則,結合這些原則,再回頭去看frame,一定會發出這樣一句驚嘆:“原來如此?。。 ?/p>
frame是什么
在SwiftUI中,frame()
是一個modifier,modifier在SwiftUI中并不是真的修改了view。大多數情況下,當我們對某個view應用一個modifier的時候,實際上會創建一個新的view。
在SwiftUI中,views并沒有frame的概念,但是它們有bounds的概念,也就是說每個view都有一個范圍和大小,它們的bounds不能夠直接通過手動的方式去修改。
當某個view的frame改變后,其child的size不一定會變化,比如,我們修改一個容器VStack
的寬度后,其內部child的布局有可能變化,也有可能不變化。我們會在下邊驗證這個說法。
大家記住這句話,每個view對自己需要的size,都有自己的想法,這是我們下邊內容講解的核心思想。
Behaviors
在SwfitUI中,view在計算自己size的時候會有不同的行為方式,我們分為4類:
- 類似于
Vstack
,它們會盡可能讓自己內部的內容展示完整,但也不會多要其他的額外空間 - 類似于
Text
這種只返回自身需要的size,如果size不夠,它非常聰明的做一些額外的操作,比如換行等等 - 類似于
Shape
這種給多大尺寸就使用多大尺寸 - 還有一些可能超出父控件的view
還存在其他一些比較特殊的例外,比如Spacer
,他的特性跟他屬于哪個容器或者哪個軸有關系。當他在VStack
中時,他會盡可能的占據剩余垂直的全部空間,而占據的水平空間為0,在HStack
中,他的行為卻又恰恰相反。
我們在下一小節的布局原則中,就會看到這些不同行為的表現了。
布局原則
大家仔細思考我接下來的這3句話:
- 當布局某個view時,其父view會給出一個建議的size
- 如果該view存在child,那么就拿著這個建議的尺寸去問他的child,child根據自身的behavior返回一個size,如果沒有child,則根據自身的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一樣的:
我們在此基礎上再增加一點難度:
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,因此他繼續拿著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,這跟我們平時開發的思維有所不同。
了解了這些布局的知識后,我們再往下看文章,就不會有那么的疑惑,在平時的開發中,對于出現比較奇怪的布局問題,也能知道造成這些問題的原因是什么了。
基本用法
我們在開發中,使用frame最頻繁的方法是:
func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center)
我們之前寫了一篇專門講解alignment的文章;SwiftUI之AlignmentGuides,沒有看過的同學一定要去看一下, 在SwiftUI中,理解Alignment Guides的用法,能夠讓我們開發效果更加高效。
當我們修改了width或者height的時候,大多數情況下布局的效果跟我們想象中的一樣,表面上看,我們通過這個方法能夠設置width和height,實際上frame本質上并不能直接修改view的size。
我們在上一小節,演示了布局的3個步驟,frame恰恰能夠改變父或者子的size值,當view詢問child的時候,如果遇到frame,則直接使用該size作為child返回的size。
接下來我們演示一個小demo, 當我們修改父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,因此,上邊顯示的效果正好符合我們的預期。
其他用法
出了上邊的基本用法外,還有下邊這樣的用法:
func frame(minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, maxWidth: CGFloat? = nil, minHeight: CGFloat? = nil, idealHeight: CGFloat? = nil, maxHeight: CGFloat? = nil, alignment: Alignment = .center)
很明顯,這么多參數可以分為3組:
- minWidth,idealWidth,maxWidth
- minHeight,idealHeight,maxHeight
- alignment
最后一組我們在其他文章中已經講的很明白了,第一組和第二組在原理上基本相同,我們重點拿出第一組來做一個詳細的講解。
當我們給minWidth,idealWidth,maxWidth賦值的時候,一定要遵循數值遞增原則,否則,xcode會給出錯誤提示。
minWidth表示的是最小的寬度, idealWidth表示最合適的寬度,maxWidth表示最大的寬度,通常如果我們用到了該方法,我們只需要考慮minWidth和maxWidth就行了。
在計算size的時候,他們遵循下邊這個流程:
其實,如果大家理解了布局的3個原則,那么理解這個流程就很簡單了,frame modifier通過計算minWidth,maxWidth和child size ,就可以看著上邊的規則返回一個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)
}
}
上邊的代碼中,我們同時設置了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)
}
}
如果只設置了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)
}
}
只要設置了maxWidth,background返回的就是maxWidth的值。
關于這里流程的各種各樣的情況,大家只需要自己寫一點代碼實驗一下就行了,總之,按照前邊說的布局3大原則來理解布局就行了。
Fixed Size Views
我們一定見過 .fixedSize()`這個modifier,表面上看,他好像應該是用在Text上的,用來固定Text的寬度,相信很多同學應該是這個想法,在這一小節,我們就會徹底理解它究竟是怎樣一個東西。
func fixedSize() -> some View
func fixedSize(horizontal: Bool, vertical: Bool) -> some View
在SwiftUI中,任何View都可以用這個modifer,當我們應用了該modifier后,布局系統在返回size的時候,就會返回與之對應的idealWIdth或者idealHeight。
我們先看一段代碼:
struct ContentView: View {
var body: some View {
Text("這個文本還挺長的,到達了一定字數后,就超過了一行的顯示范圍了!!!")
.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相同。這個結果符合我們分析的結果。
我們修改一下代碼:
struct ContentView: View {
var body: some View {
Text("這個文本還挺長的,到達了一定字數后,就超過了一行的顯示范圍了?。。?)
.fixedSize(horizontal: true, vertical: false)
.border(Color.blue)
.frame(width: 200, height: 100)
.border(Color.green)
.font(.title)
}
}
可以看到,綠框沒有任何變化,籃框變寬了,當在水平方向上應用了fixedSize時,.border(Color.blue)
在詢問child的size時,child會返回它的idealWidth,我們并沒有給出一個指定的idealWidth,每個view里邊都有自己的idealWidth。
那么我們驗證下,我們給它顯式的指定一個idealWidth:
struct ContentView: View {
var body: some View {
Text("這個文本還挺長的,到達了一定字數后,就超過了一行的顯示范圍了!?。?)
.frame(idealWidth: 300)
.fixedSize(horizontal: true, vertical: false)
.border(Color.blue)
.frame(width: 200, height: 100)
.border(Color.green)
.font(.title)
}
}
可以看出來,完全符合我們預想的結果,因此,當我們想要固定某個view的某個軸的尺寸的時候,fixedSize這個modifier是一個利器。
應用
原作者寫了一個演示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來說,系統會按照順序進行布局,當我們使用.layourPriority()
修改了布局的優先級后,系統則優先布局高優先級的view。
struct ContentView: View {
var body: some View {
VStack {
Text("床前明月光,疑是地上霜")
.background(Color.green)
Text("舉頭望明月,低頭思故鄉")
.background(Color.blue)
}
.frame(width: 100, height: 100)
}
}
可以看出來,這2個text的優先級是相同的,因此他們平分布局空間,我們給第2個text提升一點優先級:
struct ContentView: View {
var body: some View {
VStack {
Text("床前明月光,疑是地上霜")
.background(Color.green)
Text("舉頭望明月,低頭思故鄉")
.background(Color.blue)
.layoutPriority(1)
}
.frame(width: 100, height: 100)
}
}
可以明顯的看出來,優先布局第2個text。符合我們的預期。
總結
這篇文章中,講解了frame的用法,fixedSize和layoutPriority的用法,要想理解這些用法,必須理解布局的3大原則:
- 父view提供一個建議的size
- view根據自身的特點再結合它的child計算出一個size
- 使用該size在父view中布局
*注:上邊的內容參考了網站https://swiftui-lab.com/frame-behaviors/,如有侵權,立即刪除。