前言
SwiftUI
的各種堆棧是許多框架中最基本的布局工具闷游,能夠讓我們定義組視圖娘赴,這些組視圖可以按照水平饵撑、垂直或覆蓋視圖對齊撩笆。
當(dāng)涉及到水平和垂直的變體時(shí)( HStack
和 VStack
),我們需要在這兩者之間動態(tài)的切換抒线。舉個(gè)例子班巩,假如我們正在構(gòu)建一個(gè) app
其中包含 LoginActionsView
,一個(gè)讓用戶登錄時(shí)在列表中選擇操作的類:
struct LoginActionsView: View {
...
var body: some View {
VStack {
Button("Login") { ... }
Button("Reset password") { ... }
Button("Create account") { ... }
}
.buttonStyle(ActionButtonStyle())
}
}
struct ActionButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.fixedSize()
.frame(maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
}
}
以上代碼中嘶炭,我們用到了
fixedSize
防止按鈕文本被截?cái)啾Щ牛@僅是在我們確信給定的內(nèi)容視圖不會比視圖本身更大的情況。想了解更多信息眨猎,可以查看我的文章 - SwiftUI 布局系統(tǒng)第三章
目前抑进,我們的按鈕是垂直排列的,并且填滿了水平線上的可用空間(你可以用以上示例代碼預(yù)覽按鈕的樣子)睡陪,雖然這在豎向的 iPhone 上看起來很好寺渗,但假設(shè)我們現(xiàn)在想要在橫向模式下讓 UI
橫向排列。
GeometryReader 能實(shí)現(xiàn)嗎兰迫?
一種方式是用 GeometryReader
測量當(dāng)前可用空間信殊,并根據(jù)寬度是否大于其高度,可以選擇使用 HStack
或 VStack
來渲染內(nèi)容逮矛。
雖然可以在 LoginActionsView
中放入該邏輯鸡号,但我們希望以后能復(fù)用代碼,因此需要重新創(chuàng)建一個(gè)專門的視圖须鼎,作為一個(gè)獨(dú)立的組件來實(shí)現(xiàn)動態(tài)堆棧的切換邏輯鲸伴。
為了使代碼可用性更高,我們不會硬編碼讓兩個(gè)堆棧變體使用對齊或間距什么的晋控。相反汞窗,讓我們像 SwiftUI
一樣,對這些屬性參數(shù)化赡译,同時(shí)設(shè)定框架所使用的默認(rèn)值 — 就像這樣:
struct DynamicStack<Content: View>: View {
var horizontalAlignment = HorizontalAlignment.center
var verticalAlignment = VerticalAlignment.center
var spacing: CGFloat?
@ViewBuilder var content: () -> Content
var body: some View {
GeometryReader { proxy in
Group {
if proxy.size.width > proxy.size.height {
HStack(
alignment: verticalAlignment,
spacing: spacing,
content: content
)
} else {
VStack(
alignment: horizontalAlignment,
spacing: spacing,
content: content
)
}
}
}
}
}
由于我們使新的 DynamicStack
使用了與 HStack
和 VStack
相同的 API
仲吏,現(xiàn)在可以在 LoginActionsView
中直接將以前的 VStack
換成新的自定義的實(shí)例:
struct LoginActionsView: View {
...
var body: some View {
DynamicStack {
Button("Login") { ... }
Button("Reset password") { ... }
Button("Create account") { ... }
}
.buttonStyle(ActionButtonStyle())
}
}
優(yōu)秀!然而,就像上面的代碼展示的那樣裹唆,使用 GeometeryReader
來展示動態(tài)切換有一個(gè)相當(dāng)明顯的缺點(diǎn)誓斥,在幾何圖形閱讀器中總是會填充水平和垂直方向的所有可用空間(以便測量實(shí)際空間)。在我們的例子中许帐,LoginActionsView
不再只是水平方向的排列劳坑,它現(xiàn)在也能移動到屏幕的頂部。
雖然我們也有很多方法能解決這些問題(例如使用類似在這篇 Q&A 中用來使多個(gè)視圖具有相同寬度和高度的技術(shù))成畦,但真正的問題是當(dāng)我們要動態(tài)的確定方向時(shí)距芬,測量可用空間是否是一個(gè)好的方法。
一個(gè)使用尺寸類的例子
相反循帐,讓我們使用 Apple
的尺寸類系統(tǒng)來決定 DynamicStack
應(yīng)該在底層使用 HStack
還是 VStack
框仔。這樣做的好處不僅僅是在引入 GeometeryReader
之前保留同樣緊湊的布局,并且會使 DynamicStack
在開始的時(shí)候以一種和系統(tǒng)組件類似的方式在所有設(shè)備和方向上構(gòu)建拄养。
為了觀察當(dāng)前水平方向的尺寸离斩,我們需要用到 SwiftUI 環(huán)境系統(tǒng) — 通過在 DynamicStack
中聲明 @Environment
- 標(biāo)記屬性(帶有 horizontalSizeClass
關(guān)鍵路徑),將會使我們在視圖內(nèi)容中切換到當(dāng)前 sizeClass
的值:
struct DynamicStack<Content: View>: View {
...
@Environment(\.horizontalSizeClass) private var sizeClass
var body: some View {
switch sizeClass {
case .regular:
hStack
case .compact, .none:
vStack
@unknown default:
vStack
}
}
}
private extension DynamicStack {
var hStack: some View {
HStack(
alignment: verticalAlignment,
spacing: spacing,
content: content
)
}
var vStack: some View {
VStack(
alignment: horizontalAlignment,
spacing: spacing,
content: content
)
}
}
經(jīng)過以上操作衷旅,LoginActionsView
將可以在常規(guī)的尺寸渲染時(shí)動態(tài)切換成水平布局(例如在大尺寸的 iPhone
使用橫屏捐腿,或者全屏 iPad
上的任一方向)纵朋,而其它所有尺寸的配置使用垂直布局柿顶。所有這些仍然使用緊湊垂直布局,它使用的空間不超過渲染其內(nèi)容所需的空間操软。
使用布局協(xié)議
雖然我們最后已經(jīng)用了非常棒的解決方案嘁锯,可以在所有支持 SwiftUI
的 iOS
版本中使用,但也讓我們來探索一下在 iOS 16
中引入的一些新的布局工具(在寫這篇文章時(shí)聂薪,它作為 Xcode 14
的一部分仍在測試階段)
其中一個(gè)工具是新的 Layout
協(xié)議家乘,它既能讓我們創(chuàng)建完整的自定義布局,直接集成到 SwiftUI
的布局系統(tǒng)中藏澳,同時(shí)也提供給我們一種更絲滑更動畫的方式在各種布局之間動態(tài)切換 仁锯。
這都是因?yàn)槭聦?shí)證明 Layout
不僅僅是我們第三方開發(fā)者的 API
,Apple
也讓 SwiftUI
自己的布局容器使用這個(gè)新協(xié)議 翔悠。所以业崖,與其直接使用 HStack
和 VStack
作為容器視圖,不如將它們作為符合 Layout
的實(shí)例蓄愁,使用 AnyLayout
類型進(jìn)行包裝 — 就像這樣:
private extension DynamicStack {
var currentLayout: AnyLayout {
switch sizeClass {
case .regular, .none:
return horizontalLayout
case .compact:
return verticalLayout
@unknown default:
return verticalLayout
}
}
var horizontalLayout: AnyLayout {
AnyLayout(HStack(
alignment: verticalAlignment,
spacing: spacing
))
}
var verticalLayout: AnyLayout {
AnyLayout(VStack(
alignment: horizontalAlignment,
spacing: spacing
))
}
}
以上的操作是可行的双炕,因?yàn)楫?dāng) HStack
和 VStack
的內(nèi)容類型是 EmptyView
時(shí),它們都符合新的 Layout
協(xié)議(當(dāng)內(nèi)容為空時(shí)就是這種情況)撮抓,讓我們來看一下SwiftUI
的 公共接口
struct DynamicStack<Content: View>: View {
...
var body: some View {
currentLayout(content)
}
}
注意:由于回歸妇斤,
Xcode 14 beta 3
中省略了以上條件的一致性,根據(jù)SwiftUI
團(tuán)隊(duì)的 Matt Ricketson 的說法,可以直接使用底層的_HStackLayout
和_VStackLayout
類型作為臨時(shí)的解決方法站超。并希望能在未來測試版本中修復(fù)荸恕。
現(xiàn)在我們能通過使用新的 currentLayout
解決使用什么布局,現(xiàn)在我們來更新 body
的實(shí)現(xiàn)死相,簡單調(diào)用從該屬性返回的 AnyLayout
戚炫,就像函數(shù)一樣 — 像這樣:
struct DynamicStack<Content: View>: View {
...
var body: some View {
currentLayout(content)
}
}
我們之所以能像一個(gè)函數(shù)一樣調(diào)用布局方法(盡管它實(shí)際上是一個(gè)結(jié)構(gòu))是因?yàn)?
Layout
協(xié)議使用了Swift
”像函數(shù)一樣調(diào)用“ 的特性
那么我們之前的方案和上面基于布局的方案有什么區(qū)別呢?關(guān)鍵的區(qū)別在于(除了后者需要 iOS 16
)切換布局可以保留正在渲染的底層視圖的標(biāo)識媳纬,而在 HStack
和 VStack
之間切換就不會這樣双肤。這樣做會令動畫更流暢,例如在切換設(shè)備方向時(shí)钮惠,我們也有可能在執(zhí)行此類更改時(shí)獲得小幅的性能提升(因?yàn)?SwiftUI
總是在其視圖層次結(jié)構(gòu)為靜態(tài)時(shí)盡可能表現(xiàn)最佳)
選擇合適的視圖
但我們還沒有結(jié)束茅糜,因?yàn)?iOS 16
也給了我們其他有趣的新的布局工具,它有可能也能用于實(shí)現(xiàn) DynamicStack
— 一種全新的視圖類型素挽,名字叫做 ViewThatFits
蔑赘。就像字面意思一樣,這種新的容器將會在我們初始化時(shí)傳遞的候選列表中预明,基于當(dāng)前上下文挑選出最優(yōu)視圖缩赛。
在我們的例子中,這意味著我們能同時(shí)把 HStack
和 VStack
傳遞給它撰糠,并且代表我們在它們中間自動切換酥馍。
struct DynamicStack<Content: View>: View {
...
var body: some View {
ViewThatFits {
HStack(
alignment: verticalAlignment,
spacing: spacing,
content: content
)
VStack(
alignment: horizontalAlignment,
spacing: spacing,
content: content
)
}
}
}
注意:在這種情況下,我們首先放置 HStack
是很重要的阅酪,因?yàn)?VStack
可能總是合適的旨袒,即使在我們希望布局是橫向的情況下(例如 iPad
的全屏模式)。同樣重要的是要指出术辐,上述基于 ViewThatFits
的技術(shù)將會始終嘗試 HStack
砚尽,即使在用緊湊尺寸渲染布局時(shí)也是如此,只有在 HStack
不適合時(shí)才會選擇基于VStack
的布局辉词。
結(jié)語
以上就是通過四種不同的方式實(shí)現(xiàn) DynamicStack
視圖必孤,它可以根據(jù)當(dāng)前內(nèi)容在 HStack
和 VStack
之間動態(tài)切換。
關(guān)于我們
我們是由 Swift 愛好者共同維護(hù)瑞躺,我們會分享以 Swift 實(shí)戰(zhàn)敷搪、SwiftUI、Swift 基礎(chǔ)為核心的技術(shù)內(nèi)容隘蝎,也整理收集優(yōu)秀的學(xué)習(xí)資料购啄。
后續(xù)還會翻譯大量資料到我們公眾號,有感興趣的朋友嘱么,可以加入我們狮含。