SwiftUI 之 HStack 和 VStack 的切換

前言

SwiftUI 的各種堆棧是許多框架中最基本的布局工具闷游,能夠讓我們定義組視圖娘赴,這些組視圖可以按照水平饵撑、垂直或覆蓋視圖對齊撩笆。

當(dāng)涉及到水平和垂直的變體時(shí)( HStackVStack ),我們需要在這兩者之間動態(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ù)寬度是否大于其高度,可以選擇使用 HStackVStack 來渲染內(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 使用了與 HStackVStack 相同的 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)用了非常棒的解決方案嘁锯,可以在所有支持 SwiftUIiOS 版本中使用,但也讓我們來探索一下在 iOS 16 中引入的一些新的布局工具(在寫這篇文章時(shí)聂薪,它作為 Xcode 14 的一部分仍在測試階段)

其中一個(gè)工具是新的 Layout 協(xié)議家乘,它既能讓我們創(chuàng)建完整的自定義布局,直接集成到 SwiftUI 的布局系統(tǒng)中藏澳,同時(shí)也提供給我們一種更絲滑更動畫的方式在各種布局之間動態(tài)切換 仁锯。

這都是因?yàn)槭聦?shí)證明 Layout 不僅僅是我們第三方開發(fā)者的 APIApple 也讓 SwiftUI 自己的布局容器使用這個(gè)新協(xié)議 翔悠。所以业崖,與其直接使用 HStackVStack 作為容器視圖,不如將它們作為符合 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) HStackVStack 的內(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)識媳纬,而在 HStackVStack 之間切換就不會這樣双肤。這樣做會令動畫更流暢,例如在切換設(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í)把 HStackVStack 傳遞給它撰糠,并且代表我們在它們中間自動切換酥馍。

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)容在 HStackVStack 之間動態(tài)切換。

關(guān)于我們

我們是由 Swift 愛好者共同維護(hù)瑞躺,我們會分享以 Swift 實(shí)戰(zhàn)敷搪、SwiftUI、Swift 基礎(chǔ)為核心的技術(shù)內(nèi)容隘蝎,也整理收集優(yōu)秀的學(xué)習(xí)資料购啄。

后續(xù)還會翻譯大量資料到我們公眾號,有感興趣的朋友嘱么,可以加入我們狮含。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末顽悼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子几迄,更是在濱河造成了極大的恐慌蔚龙,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件映胁,死亡現(xiàn)場離奇詭異木羹,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)解孙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門坑填,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人弛姜,你說我怎么就攤上這事脐瑰。” “怎么了廷臼?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵苍在,是天一觀的道長。 經(jīng)常有香客問我荠商,道長寂恬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任莱没,我火速辦了婚禮初肉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘郊愧。我一直安慰自己朴译,他們只是感情好井佑,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布属铁。 她就那樣靜靜地躺著,像睡著了一般躬翁。 火紅的嫁衣襯著肌膚如雪焦蘑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天盒发,我揣著相機(jī)與錄音例嘱,去河邊找鬼。 笑死宁舰,一個(gè)胖子當(dāng)著我的面吹牛拼卵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蛮艰,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼腋腮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起即寡,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤徊哑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后聪富,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體莺丑,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年墩蔓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了梢莽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,117評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡奸披,死狀恐怖蟹漓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情源内,我是刑警寧澤葡粒,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站膜钓,受9級特大地震影響嗽交,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜颂斜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一夫壁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧沃疮,春花似錦盒让、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至俊啼,卻和暖如春肺缕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背授帕。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工同木, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人跛十。 一個(gè)月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓彤路,卻偏偏與公主長得像,于是被迫代替她去往敵國和親芥映。 傳聞我的和親對象是個(gè)殘疾皇子洲尊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評論 2 355

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