在學(xué)習(xí)
SwiftUI
的過程中拳缠,首先會學(xué)到的modifiers
可能就是.frame
逻淌,關(guān)·modifiers
這里有必要提一下 祭埂,其實SwiftU
中的modifies
并不是改變View
上的某個屬性而是用一個帶有相關(guān)屬性的新View
來包裝原有的View
趴捅,當(dāng)然.frame
也不例外套硼,具體的modifiers
的使用可以見我的另外一篇文章卡辰,本篇文章主要弄清楚.frame
在SwiftUI
布局體系中到底扮演著什么重要的角色。
基本View
SwiftUI
的布局原則其實很簡單:對于層級中的View
邪意,SwiftUI
都會提供一個建議尺寸 九妈,然后View
將自己布局在這個建議尺寸的可用空間內(nèi),并報告自己的實際尺寸雾鬼,默認(rèn)情況下系統(tǒng)將View
置于可用空間的中心萌朱。但是View
的實際尺寸和收到的建議尺寸是會有出入的,比如實際尺寸比建議尺寸大該如何顯示策菜?實際尺寸比建議尺寸小呢晶疼?有多的空間時該如何分配給各個View
呢?這些都和SwiftUI
中的基本View
有關(guān)又憨,下面我們從基本的View
入手翠霍。
stack
Stack
是盡可能的占據(jù)更多的空間在滿足自己的內(nèi)容,可不會委屈它們蠢莺。
Text
Text
的行為和UIKit
中的差不多寒匙,這類View
的特點是及其''老實本分'',根據(jù)自身內(nèi)容來占據(jù)應(yīng)有的尺寸躏将,當(dāng)空間不夠時像Text
就寧愿改變自己也不會超出父View
當(dāng)初提議的尺寸蒋情,比如寬度不夠時則截斷文本顯示,高度不夠時就顯示單行耸携,他們盡可能的尊重提議的尺寸。
Text("hello there, this is a long line that won't fit parent's size.")
.border(Color.blue)
.frame(width: 200, height:30)
.border(Color.green)
.font(.title)
.padding(0)
由于.frame
包裝后的View
向Text
提議的尺寸是寬度為200
高度為30
辕翰,但是Text
的實際寬度是大于提議尺寸的200
的夺衍,為了"尊重"提議的尺寸,Text
截斷自身顯示喜命,當(dāng)然也是可以忽略提議尺寸的沟沙,后面會解釋到。
Image
默認(rèn)情況下Image
的尺寸是固定的壁榕,其會忽略掉布局系統(tǒng)建議的尺寸而總是返回圖片的尺寸矛紫。要讓一個圖片 view
尺寸可變,或者說牌里,想讓它能接受建議尺寸颊咬,并將圖片適配顯示在這個空間里务甥,我們可以在它上面調(diào)用 .resizable
,默認(rèn)情況下喳篇,這會拉伸圖片敞临,讓它填滿整個建議尺寸的空間 (我們也可以設(shè)置成以瓷磚平鋪的方式,或者只拉伸圖片的某個部分來進(jìn)行填充)麸澜,因為大部分的圖片應(yīng)該是需要以固定的高寬比展示的挺尿,所以 .aspectRatio
經(jīng)常被直接搭配在 .resizable
后面組合使用。
.aspectRatio
修飾器會去獲取建議尺寸炊邦,并且基于給定的高寬比编矾,創(chuàng)建一個能最大限度填滿建議尺寸的新的尺寸值。接下來馁害,它會將這個尺寸建議給大小可變的圖像 (resizable
的圖像會填滿整個建議尺寸)窄俏,并將該尺寸返回給上層View
。我們可以選擇適配或者填充這個建議尺寸蜗细,我們也可以決定是要指定一個高寬比裆操,還是將高寬比留給子View
去做決定。
let image = Image(systemName: "ellipsis")
HStack {
image
image.resizable()
image.resizable().aspectRatio(contentMode: .fit)
}
Path
Path
類型代表了一組 2D
的繪制指令 (和Cocoa
中的 CGPath
類似)炉媒,它總會將建議的尺寸作為實際尺寸返回踪区,如果所建議的某個方向的值為 nil
,那么它返回默認(rèn)值10
吊骤。
Path { p in
p.move(to: CGPoint(x: 50, y: 0))
p.addLines([
CGPoint(x: 100, y: 75),
CGPoint(x: 0, y: 75),
CGPoint(x: 50, y: 0)
])
}
可能上面的話你不太理解缎岗,那么我在寫的詳細(xì)點,下面的這段代碼你能想到為什么是這個效果嗎白粉?
var body: some View {
VStack(spacing:0) {
Text("hello there, this is a long line that won't fit parent's size.")
.frame(width: 200, height:30)
.border(Color.green)
.font(.title)
.padding(0)
Path { p in
p.move(to: CGPoint(x: 50, y: 0))
p.addLines([
CGPoint(x: 100, y: 75),
CGPoint(x: 0, y: 75),
CGPoint(x: 50, y: 0)
])
}
}
.border(Color.red)
}
布局流程解析:
- 首先
VStack
會將整個屏幕尺寸建議給它的子Views
传泊,這里就是Text
和Path
, -
Text
在顯示完自己的內(nèi)容后上報自己所需要的尺寸鸭巴,Path
就比較特別眷细,上面我們已經(jīng)說到它總會將建議的尺寸作為實際尺寸返回,所以這里它會返回建議尺寸鹃祖,那么建議尺寸是什么溪椎?其實就是當(dāng)初VStack
建議的尺寸減去Text
所用的尺寸后的尺寸,因為VStack
是盡可能占用更多的空間的恬口,所以VStack
收到了Text
和Path
上報的尺寸后就確定了自己的尺寸校读,上圖中紅色邊框顯示的區(qū)域。
如果Path所建議的某個方向的值為nil祖能,那么它返回的默認(rèn)值為10歉秫,采用.fixedSize可以把建議尺寸設(shè)置為nil
var body: some View {
VStack(spacing:0) {
Text("hello there, this is a long line that won't fit parent's size.")
.frame(width: 200, height:30)
.border(Color.green)
.font(.title)
.padding(0)
Path { p in
p.move(to: CGPoint(x: 50, y: 0))
p.addLines([
CGPoint(x: 100, y: 75),
CGPoint(x: 0, y: 75),
CGPoint(x: 50, y: 0)
])
}
.fixedSize(horizontal: false, vertical: true)
}
.border(Color.red)
}
上面的代碼只是利用fixedsize
忽略了Path
豎直方向的建議尺寸,那么Path
在此方向的上報尺寸則會忽略父View
的建議尺寸和直接返回10
养铸,所以VStack
收到的Path
的高度尺寸為10
雁芙,所以紅色邊框的區(qū)域在高度方向會如下所示轧膘,Path
繪制在了VStack
的外面。
Shape
和Path
一樣 Shape
也總會將建議的尺寸進(jìn)行返回却特,在某個方向上建議尺寸為nil
時返回默認(rèn)值10
扶供,Shape
會盡可能的將自身繪制在建議尺寸甚至填滿建議尺寸,像是 Rectangle
裂明、Circle
椿浓、Ellipse
和 Capsule
這些內(nèi)建的形狀,會將它們自身繪制在建議尺寸中闽晦。那些沒有高寬比約束的形狀扳碍,像是 Rectangle
,會選擇填滿整個可用的空間仙蛉,在布局過程中笋敞,Shape
會接收到 path(in:)
的調(diào)用,其中的 rect
參數(shù)所包含的尺寸正是建議尺寸荠瘪。
struct Triangle: Shape {
func path(in rect: CGRect) -> Path {
return Path { p in
p.move(to: CGPoint(x: rect.midX, y: rect.minY))
p.addLines([
CGPoint(x: rect.maxX, y: rect.maxY),
CGPoint(x: rect.minX, y: rect.maxY),
CGPoint(x: rect.midX, y: rect.minY)
])
}
}
}
Frame的使用
方式一
在使用
Frame
的過程中很容易以為Frame
設(shè)置的多大夯巷,View
就會占據(jù)多少空間,其實Frame
改變的只是建議尺寸哀墓,具體View
的實際尺寸是要看View
怎么來繪制自己的趁餐,上面我們已經(jīng)介紹了基礎(chǔ)的View
,可以發(fā)現(xiàn)基礎(chǔ)View
在收到建議尺寸時會有不同的表現(xiàn)形式篮绰,有的是尊重建議尺寸后雷,有的則填充建議尺寸,還有的甚至可以在建議尺寸外面繪制自己吠各。
Frame
一共有二個初始化的方法:
func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center)
func frame(minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, maxWidth: CGFloat? = nil, minHeight: CGFloat? = nil, idealHeight: CGFloat? = nil, maxHeight: CGFloat? = nil, alignment: Alignment = .center)
在使用時參數(shù)是可變的臀突,可以只設(shè)置width
或者height
,也可以同時設(shè)置贾漏,其中關(guān)于aligment
的內(nèi)容請看我的另外一篇文章候学。
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))
}
}
}
代碼解讀
- 首先
VStack
會將整個屏幕尺寸建議給它的子View
,其中包括SubView
纵散,Text
和Slider
盒齿,當(dāng)然這里涉及到分配原則,比如有多的空間和空間不足時該如何分配的問題困食,這里我們先不談(后面會出文章專門聊這個)。 -
SubView
經(jīng)過.frame
修飾器修飾過后收到的建議寬度尺寸為width
變量翎承,當(dāng)我們滑動Slider
時會改變width
變量的值硕盹。 - 當(dāng)
width
變量的值小于120
時,在SubView
內(nèi)部利用GeometryReader
讀取了這個width
變量的建議寬度叨咖,由于采用max
取的是最大值瘩例,所以SubView
內(nèi)部的Rectangle
會收到建議尺寸的寬度是120
啊胶。 - 上面我們已經(jīng)講過,對于
Shape
來說垛贤,Rectangle
會在指定了建議尺寸時會填滿建議尺寸的焰坪,所以就算當(dāng)width變量的值小于120
,由于Rectangle
填滿了寬度120
的尺寸聘惦,并將當(dāng)初建議的寬度120
尺寸直接返回了某饰,所以SubView
的寬度也為120
了。 - 當(dāng)
width
變量大于120
時善绎,也很好理解黔漂,Rectangle
依然填滿建議尺寸,所以SubView
會跟著變寬禀酱。
上面的整個布局流程是一層層由外到內(nèi)炬守,然后由內(nèi)到外確定的,這種思想和
UIKit
是有很大的不同的剂跟,只有清理的知道SwiftUI
內(nèi)部的布局流程减途,才能不會對有些實際出來的布局效果和自己所想有所出入時感到驚訝,上面的例子請好好體會曹洽,因為接下來會進(jìn)階到更難的鳍置。
方式二
上面的例子使用的是Frame
的初始化方法1
,對于初始化方法2
則是指定最小衣洁,理想和最大尺寸墓捻,其中我們在傳值時minimum
,ideal
和maximum
必須按照升序的方式傳入坊夫,不然會報錯砖第,我們可以將任意參數(shù)留空,這樣它們會使用默認(rèn)的nil
值环凿,某個方向上的最小值和最大值將作為建議尺寸和返回尺寸的鉗位梧兼,舉例來說,當(dāng)我們對最大寬度進(jìn)行了配置時智听,.frame
修飾器會去檢查被建議的寬度羽杰,如果這個被建議的寬度超過了設(shè)置的最大寬度,那么它只會將最大寬度建議給它的子 view
到推。類似地考赛,如果子view
返回了一個比最大寬度更大的寬度,那么這個結(jié)果也將被鉗至最大寬度值莉测,當(dāng)在某個方向設(shè)置了fixedsize
時颜骤,此時會采用ideal
尺寸進(jìn)行布局。
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)
}
}
.border(Color.orange).frame(height:proxy.size.height)
}.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))
}
}
上面這個例子是一個綜合的例子捣卤,模擬的是類似Text
的效果忍抽,當(dāng)空間足夠時則顯示7
個小方格八孝,當(dāng)空間不夠時小方格盡可能的顯示,并將顏色變成紅色鸠项,下面是效果圖干跛,建議再看代碼截圖前,自己嘗試?yán)硐律鲜龃a的布局流程祟绊。
代碼解讀
- 當(dāng)
Toggle
沒有打開時楼入,fixedSize
這個變量為false
,裝著LittleSquares的VStack
容器通過.frame
給LittleSquares
傳遞了指為變量width
的寬度建議尺寸久免。 - 由于
fixedSize
為false
浅辙, 所以不會啟動ideal
尺寸參與布局,在LittleSquares
的內(nèi)部通過GeometryReader
拿到的proxy
就是上面的變量width
的值阎姥,同時把這個值傳遞給了maxSquares
方法來計算能最大顯示方格的數(shù)量记舆,封頂數(shù)量是7
個,allFit
方法則簡單根據(jù)顯示的數(shù)量是否為7
個改變了方格的顏色呼巴。 - 注意到
GeometryReader
設(shè)置了frame
泽腮,當(dāng)它收到的建議尺寸寬度為nil
時會將對內(nèi)部HStack
的建議尺寸用idealWidth
代替,此時由于fixedSize
為false
衣赶,所以不會使用idealWidth
的值建議給內(nèi)部的HStack
诊赊,而是采用maxWidth
的值,當(dāng)它收到的建議尺寸的寬度超過了這個maxWidth
的時候它將建議尺寸的寬度鉗至maxWidth
的值傳遞給內(nèi)部的HStack
府瞄,如果小于maxWidth
凯楔,則直接把收到的建議寬度尺寸傳遞給內(nèi)部的HStack
灸叼,同時在收到內(nèi)部HStack
上報返回的尺寸時酬核,也會利用這個規(guī)則赦肃,如果超過了maxWidth
則也會被鉗至maxWidth
這個值。 - 所以當(dāng)滑動
Slider
使紅色的矩形框變大時货邓,矩形也只會最多顯示7
個秆撮,但是由于沒有設(shè)置minWidth
,所以當(dāng)紅色的矩形框變小時則不足以顯示7
個换况,會根據(jù)收到的建議尺寸空間盡量的顯示更多的方格职辨。 - 當(dāng)
Toggle
沒有打開時,fixedSize
這個變量為true
戈二,GeometryReader
將idealWidth
傳遞給了內(nèi)部的HStack
舒裤,所以始終顯示7
個方格。
總結(jié)
本文主要是介紹了SwiftUI
的布局思想觉吭,并介紹了幾種常見的基本View
的布局原則腾供,同時介紹了.frame
修飾器的內(nèi)部實現(xiàn)原理,同時利用實際例子進(jìn)行了演示,只有理解SwiftUI
的布局思想台腥,才不會對意想不到的布局效果感到意外。