除了聲明式 DSL 和強(qiáng)大的數(shù)據(jù)綁定外畜伐,SwiftUI 還具有全新的布局系統(tǒng)灼伤,該系統(tǒng)在許多方面結(jié)合了手動(dòng)框架計(jì)算的明確性和自動(dòng)布局的適應(yīng)性搏色。乍一看可能看起來(lái)很簡(jiǎn)單的系統(tǒng)仰猖,但是一旦我們開始將其各種構(gòu)建塊組合成越來(lái)越復(fù)雜的布局芬探,它就會(huì)提供巨大的靈活性和強(qiáng)大的功能神得。
frame
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
}
}
默認(rèn)情況下,SwiftUI 允許每個(gè)視圖根據(jù)其渲染的容器選擇自己的大小偷仿,然后居中放置在其父視圖中哩簿。所以上面代碼的結(jié)果是在屏幕中央呈現(xiàn)一個(gè)小圖標(biāo)——而不是像我們基于 UIKit 和 AppKit 的工作方式所預(yù)期的那樣位于左上角或左下角宵蕉。
接下來(lái),讓我們把圖標(biāo)放大一點(diǎn)节榜,比如 50x50 pt羡玛。關(guān)于如何實(shí)現(xiàn)這一點(diǎn)的初步想法可能是使用.frame()
告訴我們的視圖采用該大小,如下所示:
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
.frame(width: 50, height: 50)
}
}
然而宗苍,雖然上面的代碼會(huì)產(chǎn)生一個(gè)50x50 pt的視圖稼稿,但我們圖標(biāo)的大小將與之前完全一樣——這乍一看可能有點(diǎn)奇怪。為了探究為什么會(huì)這樣讳窟,讓我們給我們的視圖一個(gè)背景顏色让歼,這樣我們就可以很容易地看到它在屏幕上的frame:
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
.frame(width: 50, height: 50)
.background(Color.red)
}
}
有了上面的內(nèi)容,我們可以看到我們的視圖確實(shí)是正確的大小 丽啡。只是我們的圖標(biāo)似乎完全不受我們的.frame()
修飾符的影響谋右。將修飾符應(yīng)用于視圖時(shí),我們通常根本不是修飾視圖补箍,而是將其封裝在一個(gè)新的改执、透明的視圖中。因此坑雅,在上面調(diào)用.background()
時(shí)辈挂,我們實(shí)際上是將該background modifier應(yīng)用于包裝Image的新視圖上面,而不是Image本身裹粤。
所以呢岗,從布局的角度來(lái)看,我們的Image沒(méi)有任何變化蛹尝,它仍然在其父視圖中居中,只是這次它的父視圖是一個(gè)新的 50x50 透明包裝視圖悉尾,但渲染結(jié)果看起來(lái)是一樣的突那。
在SwiftUI中 視圖負(fù)責(zé)確定它們自己的大小,我們需要告訴我們的Image調(diào)整自身大小以占用所有可用空間构眯,而不是堅(jiān)持其默認(rèn)大小愕难。為了實(shí)現(xiàn)這一點(diǎn),我們只需使用.resizable()
修飾符:
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
.resizable()
.frame(width: 50, height: 50)
.background(Color.red)
}
}
padding
接下來(lái)惫霸,我們來(lái)看看SwiftUI 中的padding猫缭。就像在其他布局系統(tǒng)中一樣,如 CSS壹店,padding 使我們能夠在其自己的frame內(nèi)偏移視圖的內(nèi)容猜丹。然而,我們?cè)趘iew modifiers中使用padding
的位置硅卢,可以獲得完全不同的渲染結(jié)果射窒。例如藏杖,讓我們通過(guò)在上述代碼的末尾附加.padding()
修飾符來(lái)應(yīng)用paddding
:
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
.resizable()
.frame(width: 50, height: 50)
.background(Color.red)
.padding()
}
}
上面的結(jié)果可能不是我們所期望的,因?yàn)槲覀兘o了calendar圖標(biāo)外邊距脉顿。 沒(méi)有背景顏色的額外空白蝌麸。想一下,這與我們之前在應(yīng)用.frame()
修飾符時(shí)遇到的情況完全相同艾疟。調(diào)用.padding()
實(shí)際上并沒(méi)有改變我們之前的視圖和修飾符来吩,它只是在前面表達(dá)式的結(jié)果周圍添加了空白而已。
事實(shí)上蔽莱,如果我們?cè)谡{(diào)用.padding()
之后添加第二個(gè).background()
修飾符弟疆,就會(huì)更加清晰的展示這種情況。因?yàn)榈诙€(gè)背景顏色將在padding內(nèi)渲染出來(lái):
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
.resizable()
.frame(width: 50, height: 50)
.background(Color.red)
.padding()
.background(Color.blue)
}
}
因此碾褂,如果我們希望添加內(nèi)邊距inner padding兽间,我們需要在添加背景之前使用padding()
修飾符——就像這樣:
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
.resizable()
.frame(width: 50, height: 50)
.padding()
.background(Color.red)
}
}
每個(gè)修飾符modifier本質(zhì)上都將它調(diào)用的視圖包裝在另一個(gè)視圖中,如果我們?cè)趹?yīng)用.frame()
修飾符之前調(diào)用.padding()
正塌,我們的圖標(biāo)將縮小嘀略,因?yàn)閜adding將應(yīng)用在我們固定的50x50
容器中——迫使我們調(diào)整圖像尺寸以適應(yīng)較小的尺寸:
struct ContentView: View {
var body: some View {
Image(systemName: "calendar")
.resizable()
.padding()
.frame(width: 50, height: 50)
.background(Color.red)
}
}
為了完成我們的calendar圖標(biāo)視圖,讓我們對(duì)其應(yīng)用cornerRadius
并使其前景色為白色——最后將所有代碼提取到一個(gè)名為CalendarView
的新視圖中乓诽,如下所示:
struct CalendarView: View {
var body: some View {
Image(systemName: "calendar")
.resizable()
.frame(width: 50, height: 50)
.padding()
.background(Color.red)
.cornerRadius(10)
.foregroundColor(.white)
}
}
一般而言帜羊,每當(dāng)我們完成一個(gè)獨(dú)立的功能組件 UI 部分時(shí),將代碼提取到新View
實(shí)現(xiàn)中通常是個(gè)好主意鸠天,以避免出現(xiàn)臃腫視圖讼育。
Stacks and spacers
SwiftUI 中的各種stacks和spacers乍一看可能非常簡(jiǎn)單和有限,實(shí)際上卻可以用來(lái)表達(dá)幾乎無(wú)限的布局組合:
struct ContentView: View {
var body: some View {
VStack {
CalendarView()
}
}
}
上述內(nèi)容VStack
實(shí)際上根本不會(huì)影響我們的布局稠集,因?yàn)?在SwiftUI中stacks不會(huì)拉伸自己以占據(jù)其父級(jí) 奶段。相反,它們只是根據(jù)其子級(jí)的總大小調(diào)整自己的大小剥纷。
要移動(dòng)我們的CalendarView
痹籍,我們還必須將Spacer
添加到我們的stacks中。在HStack
或者VStack
中放置spacers時(shí)晦鞋,spacers總是占據(jù)盡可能多的空間蹲缠,在下面代碼中,渲染后我們的CalendarView
會(huì)被推到屏幕頂部:
struct ContentView: View {
var body: some View {
VStack {
CalendarView()
Spacer()
}
}
}
stacks的酷炫之處在于它們可以通過(guò)嵌套來(lái)構(gòu)建日益復(fù)雜的布局悠垛,而無(wú)需任何形式的手動(dòng)frame計(jì)算线定。
struct ContentView: View {
var body: some View {
HStack {
VStack {
CalendarView()
Spacer()
}
Spacer()
}.padding()
}
}
接下來(lái),讓我們向視圖中添加一個(gè)Text
确买,以模擬calendar事件的一組詳細(xì)信息斤讥。由于在本文中我們將堅(jiān)持僅探索 SwiftUI 的布局系統(tǒng),因此我們現(xiàn)在將硬編碼我們的內(nèi)容Text
:
struct ContentView: View {
var body: some View {
HStack {
VStack {
CalendarView()
Spacer()
}
Text("Event title").font(.title)
Spacer()
}.padding()
}
}
看看上面的代碼湾趾,我們希望Text
出現(xiàn)在CalendarView
水平軸的右邊周偎。
struct ContentView: View {
var body: some View {
HStack(alignment: .top) {
VStack {
CalendarView()
Spacer()
}
Text("Event title").font(.title)
Spacer()
}.padding()
}
}
下面我們繼續(xù)在文字視圖下面添加一個(gè)用來(lái)描述的文字視圖:
struct ContentView: View {
var body: some View {
HStack(alignment: .top) {
VStack {
CalendarView()
Spacer()
}
VStack(alignment: .leading) {
Text("Event title").font(.title)
Text("Location")
}
Spacer()
}.padding()
}
}
然而抹剩,雖然上述布局有效,但可以被簡(jiǎn)化蓉坎,以便更容易閱讀代碼澳眷。
因此,將ContentView
的body
代碼提取到一個(gè)專用組件中蛉艾,同時(shí)對(duì)其進(jìn)行重構(gòu)并命名為EventHeader
:
struct EventHeader: View {
var body: some View {
HStack(spacing: 15) {
CalendarView()
VStack(alignment: .leading) {
Text("Event title").font(.title)
Text("Location")
}
Spacer()
}
}
}
回到我們的ContentView
钳踊,我們現(xiàn)在可以將它的主體變成一個(gè)VStack
包含我們的新EventHeader
組件以及Spacer
,使我們的布局代碼更容易理解:
struct ContentView: View {
var body: some View {
VStack {
EventHeader()
Spacer()
}.padding()
}
}
只要有可能勿侯,最好不斷地將ContenView
中的代碼提取到專用組件中拓瞪。以這種方式編碼通常可以讓我們自然地將我們的 UI 分成原子部分助琐,而無(wú)需我們預(yù)先進(jìn)行大量的架構(gòu)設(shè)計(jì)工作祭埂。
ZStacks 和 offset
最后,讓我們快速了解一下 SwiftUI 的ZStack
類型兵钮,它使我們能夠使用從后到前的順序蛆橡,在深度方面堆疊一系列視圖。
舉個(gè)例子掘譬,假設(shè)在之前的calendar視圖頂部添加顯示一個(gè)小的“驗(yàn)證徽章”的支持泰演。在 top-trailing
放置一個(gè)checkmark
圖標(biāo)。為了以更通用的方式實(shí)現(xiàn)它葱轩,讓我們對(duì)View
進(jìn)行擴(kuò)展睦焕,將所有視圖包裝在ZStack
內(nèi)(它本身不會(huì)影響視圖的布局),它還可以選擇包含我們的checkmark
圖標(biāo) :
extension View {
func addVerifiedBadge(_ isVerified: Bool) -> some View {
ZStack(alignment: .topTrailing) {
self
if isVerified {
Image(systemName: "checkmark.circle.fill")
.offset(x: 3, y: -3)
}
}
}
}
請(qǐng)注意ZStack
如何在alignment
上為我們提供對(duì)其的完整二維控制靴拱,我們可以使用它來(lái)將我們的圖標(biāo)定位在父視圖的top-trailing
垃喊。然后我們還將.offset()
modifier應(yīng)用到我們的徽章上,這會(huì)將它稍微移動(dòng)到其父視圖的邊界之外袜炕。
有了上述內(nèi)容本谜,我們現(xiàn)在可以有條件地將我們的新徽章添加到我們CalendarView
上,在eventIsVerified
屬性設(shè)置為true
時(shí)可以顯示出來(lái):
struct CalendarView: View {
var eventIsVerified = true
var body: some View {
Image(systemName: "calendar")
.resizable()
.frame(width: 50, height: 50)
.padding()
.background(Color.red)
.cornerRadius(10)
.foregroundColor(.white)
.addVerifiedBadge(eventIsVerified)
}
}
將ZStack
與.offset()
修飾符一起使用是向視圖添加各種疊加層的好方法妇蛀,而根本不會(huì)影響該視圖自己的布局。我們可以使用該技術(shù)來(lái)實(shí)現(xiàn)進(jìn)度加載笤成、應(yīng)用內(nèi)通知以及我們希望在現(xiàn)有視圖層次結(jié)構(gòu)之上呈現(xiàn)的許多其他類型的視圖评架。
結(jié)論
讓我們總結(jié)一下到目前為止我們所涵蓋的內(nèi)容:
- SwiftUI 的核心布局引擎的工作原理是要求每個(gè)子視圖根據(jù)其父視圖的邊界確定自己的大小,然后要求每個(gè)父視圖將其子視圖定位在自己的邊界內(nèi)炕泳。
- 視圖修飾符View modifiers通常將當(dāng)前視圖包裝在另一個(gè)視圖中纵诞,這就是為什么我們可以根據(jù)調(diào)用修飾符的順序獲得完全不同的布局結(jié)果。
- 使用
.frame()
和.padding()
修飾符可以讓我們調(diào)整視圖的大小和內(nèi)部邊距培遵,只要該視圖配置為相應(yīng)地調(diào)整自身大小浙芙。 - 使用
HStack
登刺,VStack
和ZStack
我們可以一起在水平方向,垂直或深度方向排列視圖嗡呼。 - 使用
offset()
我們可以移動(dòng)視圖而不影響其周圍環(huán)境纸俭,這在實(shí)現(xiàn)疊加和其他類型的視圖重疊時(shí)非常有用。