精通 SwiftUI - iOS 17 版

第 45 章
利用 AnyLayout 切換 UI 佈局

從 iOS 16 開始,SwiftUI 推出了 AnyLayoutLayout 協定,讓開發者構建客製化和複雜的 UI 佈局。AnyLayout 是 layout 協定的 type-erased 實例。我們可以使用 AnyLayout 來創建動態 UI 佈局,它可以回應使用者的互動或環境變化。

在這章節,我們會看看如何使用 AnyLayout 來切換垂直和水平佈局。

如何使用 AnyLayout

首先,讓我們用 App 模板創建一個新的 Xcode 專案,並為專案命名,我會把專案命名為 SwiftUIAnyLayout。我們會構建一個簡單的範例 App,在使用者點擊堆疊視圖時切換 UI 佈局。UI 佈局在不同方向看起來會是這樣的:

圖 45.1. 使用 AnyLayout 在垂直和水平堆棧之間切換
圖 45.1. 使用 AnyLayout 在垂直和水平堆棧之間切換

在開始時,範例 App 利用 VStack 把三個圖像垂直排列。當使用者點擊堆疊視圖時,就會變成水平堆疊。我們可以這樣使用 AnyLayout 來實作:

struct ContentView: View {
    @State private var changeLayout = false

    var body: some View {
        let layout = changeLayout ? AnyLayout(HStackLayout()) : AnyLayout(VStackLayout())

        layout {
            Image(systemName: "bus")
                .font(.system(size: 80))
                .frame(width: 120, height: 120)
                .background(in: RoundedRectangle(cornerRadius: 5.0))
                .backgroundStyle(.green)
                .foregroundStyle(.white)


            Image(systemName: "ferry")
                .font(.system(size: 80))
                .frame(width: 120, height: 120)
                .background(in: RoundedRectangle(cornerRadius: 5.0))
                .backgroundStyle(.yellow)
                .foregroundStyle(.white)

            Image(systemName: "scooter")
                .font(.system(size: 80))
                .frame(width: 120, height: 120)
                .background(in: RoundedRectangle(cornerRadius: 5.0))
                .backgroundStyle(.indigo)
                .foregroundStyle(.white)

        }
        .animation(.default, value: changeLayout)
        .onTapGesture {
            changeLayout.toggle()
        }
    }
}

我們定義了一個 layout 變數,來保存 AnyLayout 的實例。這個 layout 會根據 changeLayout 的數值,來切換水平和垂直 layout。HStackLayout(或 VStackLayout)的行為與 HStack(或 VStack)類似;但因為它符合 Layout 協定,我們就可以在 conditional layout 中使用它。

我們還可以把動畫附加到 layout,來動畫化佈局的改變。現在,當我們點擊堆疊視圖時,它就會切換垂直或水平佈局。

根據裝置的方向切換 layout

現在,範例 App 讓使用者點擊堆疊視圖來切換 layout。在某些 App 中,我們可能會想根據裝置方向和螢幕大小來切換 layout。在這個情況下,就可以利用 .horizontalSizeClass 變數來捕捉裝置方向的改變。

@Environment(\.horizontalSizeClass) var horizontalSizeClass

然後,讓我們如此更新 layout 變數:

let layout = horizontalSizeClass == .regular ? AnyLayout(HStackLayout()) : AnyLayout(VStackLayout())

舉個例子,如果我們把 iPhone 14 Pro Max 轉為橫向,layout 就會切換為橫向堆疊視圖。

圖 45.2. 當設備處於橫向時切換到水平堆棧視圖
圖 45.2. 當設備處於橫向時切換到水平堆棧視圖

在大多數情況下,我們會使用 SwiftUI 內建的 layout container 來創建 layout,像是 HStackLayoutVStackLayout。但如果這些 layout container 無法實作我們需要的 layout 型別,該怎麼辦呢?Layout 協定就讓我們可以定義自己客製化的 layout。我們只需要創建一個符合 Layout 協定的型別,並實作以下所需的方法,來定義一個客製化的 layout container:

  • sizeThatFits(proposal:subviews:cache:) - 這個方法會報告合成 layout 視圖的大小。
  • placeSubviews(in:proposal:subviews:cache:) - 這個方法會為 container 的子視圖分配位置。

總結

推出了 AnyLayout 後,我們只需要幾行程式碼就可以客製化或更改 UI layout,這絕對可以幫助我們構建更優雅和吸引的 UI。在這篇文章的範例 App 中,大家都學會了如何根據螢幕方向切換 layout。其實同樣的技術也可以應用於其他情況中,例如是 Dynamic Type 的大小。

在本章所準備的範例檔中,有最後完整的 Xcode 專案,可供你下載參考: