SwiftUI 布局系統(tǒng)指南 - 第 1 部分

除了聲明式 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")
    }
}
centered-calendar.png

默認(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)
    }
}
framed-calendar.png

有了上面的內(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)
    }
}
resized-calendar.png

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)
    }
}
padded-calendar.png

因此碾褂,如果我們希望添加內(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)
    }
}
smaller-calendar.png

為了完成我們的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)
    }
}
finished-calendar.png

一般而言帜羊,每當(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()
        }
    }
}
calendar-at-top.png

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()
    }
}
calendar-at-leading-top.png

接下來(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()
    }
}
calendar-with-text.png

看看上面的代碼湾趾,我們希望Text出現(xiàn)在CalendarView水平軸的右邊周偎。

struct ContentView: View {
    var body: some View {
        HStack(alignment: .top) {
            VStack {
                CalendarView()
                Spacer()
            }
            Text("Event title").font(.title)
            Spacer()
        }.padding()
    }
}
calendar-with-top-text.png

下面我們繼續(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)化蓉坎,以便更容易閱讀代碼澳眷。

因此,將ContentViewbody代碼提取到一個(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()
        }
    }
}
event-header.png

回到我們的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)
    }
}
verified-event.png

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登刺,VStackZStack我們可以一起在水平方向,垂直或深度方向排列視圖嗡呼。
  • 使用offset()我們可以移動(dòng)視圖而不影響其周圍環(huán)境纸俭,這在實(shí)現(xiàn)疊加和其他類型的視圖重疊時(shí)非常有用。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末南窗,一起剝皮案震驚了整個(gè)濱河市揍很,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌万伤,老刑警劉巖窒悔,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異敌买,居然都是意外死亡简珠,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門虹钮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)聋庵,“玉大人,你說(shuō)我怎么就攤上這事芜抒≌洳撸” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵宅倒,是天一觀的道長(zhǎng)攘宙。 經(jīng)常有香客問(wèn)我,道長(zhǎng)拐迁,這世上最難降的妖魔是什么蹭劈? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮线召,結(jié)果婚禮上铺韧,老公的妹妹穿的比我還像新娘。我一直安慰自己缓淹,他們只是感情好哈打,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著讯壶,像睡著了一般料仗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上伏蚊,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天立轧,我揣著相機(jī)與錄音,去河邊找鬼。 笑死氛改,一個(gè)胖子當(dāng)著我的面吹牛帐萎,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播胜卤,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼疆导,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了瑰艘?” 一聲冷哼從身側(cè)響起是鬼,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎紫新,沒(méi)想到半個(gè)月后均蜜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡芒率,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年囤耳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片偶芍。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡充择,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出匪蟀,到底是詐尸還是另有隱情椎麦,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布材彪,位于F島的核電站观挎,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏段化。R本人自食惡果不足惜嘁捷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望显熏。 院中可真熱鬧雄嚣,春花似錦、人聲如沸喘蟆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蕴轨。三九已至港谊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間尺棋,已是汗流浹背封锉。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留膘螟,地道東北人成福。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像荆残,于是被迫代替她去往敵國(guó)和親奴艾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容