SwiftUI 佈局 —— 對齊

語言: CN / TW / HK

highlight: a11y-dark

“對齊”是 SwiftUI 中極為重要的概念,然而相當多的開發者並不能很好地駕馭這個佈局利器。在 WWDC 2022 中,蘋果為 SwiftUI 增添了 Layout 協議,讓我們有了更多的機會了解和驗證 SwiftUI 的佈局原理。本文將結合 Layout 協議的內容對 SwiftUI 的 “對齊” 進行梳理,希望能讓讀者對“對齊”有更加清晰地認識和掌握。

本文並不會對 alignment 、alignmentGuide 等內容作詳盡的介紹,想了解更多的內容可以閱讀文中推薦的資料。可以在此處下載 本文所需的原始碼

原文發表在我的部落格 wwww.fatbobman.com

歡迎訂閱我的公共號:【肘子的Swift記事本】

什麼是對齊( Alignment )

對齊是發生在多個物件之間的一種行為。比如將書桌上的一摞書擺放整齊,列隊訓練時向左(右)看齊等等。在 SwiftUI 中,對齊是指在佈局容器中,將多個檢視按照對齊指南( Alignment Guide )進行對齊。比如下面的程式碼便是要求 ZStack 容器內的所有檢視,按照各自的中心點進行對齊:

swift ZStack(alignment: .center) { Text("Hello") Text("World") Circle() .frame(width: 50, height: 50) }

在“對齊”行為中最關鍵的兩點為:

  • 以什麼為對齊指南
  • 對哪些檢視進行“對齊”

對齊指南

概述

對齊指南( alignment guide)用來標識檢視間進行對齊的依據,它具備如下特點:

  • 對齊指南不僅可以標識點,還可以標識線

在 SwiftUI 中,分別用 HorizontalAlignment 和 VerticalAlignment 來標識在檢視縱軸和橫軸方向的參考線,並且可以由兩者共同構成對檢視中的某個具體的參考點的標識。

HorizontalAlignment.leading 、HorizontalAlignment.center 、HorizontalAlignment.trailing 分別標識了前沿、中心和後緣( 沿檢視水平軸 )。

VerticalAlignment.top 、VerticalAlignment.center 、VerticalAlignment.bottom 則分別標識了頂部、中心和底部( 沿檢視垂直軸 )。

而 Alignment.topLeading 則由 HorizontalAlignment.leading 和 VerticalAlignment.top 構成,兩條參考線的交叉點標識了檢視的頂部—前沿。

image-20220704154347077

image-20220704154754068

  • 對齊指南由函式構成

HorizontalAlignment 和 VerticalAlignment 本質上是一個返回型別為 CGFloat 的函式。該函式將返回沿特定軸向的對齊位置( 偏移量 )

  • 對齊指南支援多種佈局方向

正是由於對齊指南由函式構成,因此其先天便具備了靈活的適應能力。在 SwiftUI 中,系統預置對齊指南都提供了對不同佈局方向的支援。只需修改檢視的排版方向,對齊指南將自動改變其對應的位置

swift VStack(alignment:.leading){ Text("Hello world") Text("WWDC 2022") } .environment(\.layoutDirection, .rightToLeft)

image-20220629202253658

image-20220629202556777

想更多地瞭解自定義對齊指南以及 Alignment Guide 的應用案例,推薦閱讀 Javier 的 Alignment Guides in SwiftUI 一文

自定義對齊指南

除了 SwiftUI 提供的預置對齊指南外,開發者也可以自定義對齊指南:

```swift struct OneThirdWidthID: AlignmentID { static func defaultValue(in context: ViewDimensions) -> CGFloat { context.width / 3 } } // 自定義了一個 HorizontalAlignment , 該參考值為檢視寬度的三分之一 extension HorizontalAlignment { static let oneThird = HorizontalAlignment(OneThirdWidthID.self) }

// 也可以為 ZStack 、frame 定義同時具備兩個維度值的參考點 extension Alignment { static let customAlignment = Alignment(horizontal: .oneThird, vertical: .top) } ```

自定義對齊指南與 SwiftUI 預置的對齊指南一樣,可用於任何支援對齊的容器檢視。

alignmentGuide 修飾器

在 SwiftUI 中,開發者可以使用 alignmentGuide 修飾器來修改檢視某個對齊指南的值( 為對齊指南設定顯式值,有關顯式值見下文)。比如:

```swift struct AlignmentGuideDemo:View{ var body: some View{ VStack(alignment:.leading) { rectangle // Rectangle1 .alignmentGuide(.leading, computeValue: { viewDimensions in let defaultLeading = viewDimensions[.leading] // default is 0 let newLeading = defaultLeading + 30 return newLeading })

        rectangle // Rectangle2
    }
    .border(.pink)
}

var rectangle:some View {
    Rectangle()
        .fill(.blue.gradient)
        .frame(width: 100, height: 100)
}

} ```

通過 alignmentGuide 我們將 Rectangle1 的 HorizontalAlignment.leading 沿水平軸向右側偏移了 30 ,與 Rectangle2 在 VStack 中按 .leading 對齊後結果如下圖:

image-20220704171710023

對齊指南的顯式值

對齊指南值 = 顯式值 ?? 預設值

檢視中的每個對齊指南都有預設值( 通過在對齊指南定義中的 defaultValue 方法獲取 )。在不為對齊指南設定顯式值( 顯式值為 nil )的情況下,對齊指南將返回預設值。

swift Rectangle() .fill(.blue.gradient) .frame(width: 100, height: 100) // 預設的對齊指南值: // leading: 0 , HorizontalAlignment.center: 50, trailing: 50 // top: 0 , VerticalAlignment.center: 50 , bottom: 100 // firstTextBaseline : 100 , lastTextBaseline : 100

如果我們使用了 alignmentGuide 為某個對齊指南設定了顯式值,那麼此時對齊指南的值為我們設定的顯式值。

swift Rectangle() .fill(.blue.gradient) .frame(width: 100, height: 100) .alignmentGuide(.leading, computeValue: { viewDimensions in let leading = viewDimensions[.leading] // 由於此時顯式值為 nil , 因此 leading 值為 0 return viewDimensions.width / 3 // 將 leading 的顯式值設定為寬度三分之一處 }) .alignmentGuide(.leading, computeValue: { viewDimensions in let leading = viewDimensions[.leading] // 因為上面設定了顯式值,此時 leading 值為 33.33 let explicitLeading = viewDimensions[explicit: .leading] // 顯式值 , 此時為 Optional(33.33) return viewDimensions[HorizontalAlignment.center] // 再度設定 leading 的顯式值。此時顯式值為 Optional(50) , .leading 值為 50 })

即使你沒有修改對齊指南的預設值,但只要為 alignmentGuide 提供了返回值,便設定了顯式值:

swift Rectangle() .fill(.blue.gradient) .frame(width: 100, height: 100) .alignmentGuide(.leading, computeValue: { viewDimensions in let leading = viewDimensions[.leading] // 此時 leading 的顯式值為 nil return leading // 此時 leading 為 0 ,leading 的顯式值為 0 })

特殊的對齊指南

在上文中,我們故意避開了兩個容易令人困惑的對齊指南:firstTextBaseline、lastTextBaseline 。因為這兩個對齊指南會根據檢視內容的不同而變化。

在閱讀下面的程式碼時,請在心中自行分析一下檢視對應的 firstTextBaseline 和 lastTextBaseline 對齊指南的位置:

swift Rectangle() .fill(.orange.gradient) .frame(width: 100, height: 100)

image-20220629205343135

檢視中沒有文字,firstTextBaseline 和 lastTextBaseline 等同於 bottom

swift Text("Hello world") .border(.red)

image-20220704175657449

單行文字,firstTextBaseline 和 lastTextBaseline 相同。文字基線不同於 bottom

swift Text("山不在高,有仙則名。水不在深,有龍則靈。斯是陋室,惟吾德馨。苔痕上階綠,草色入簾青。談笑有鴻儒,往來無白丁。可以調素琴,閱金經。無絲竹之亂耳,無案牘之勞形。南陽諸葛廬,西蜀子云亭。孔子云:何陋之有?") .frame(width:200)

image-20220704175811856

多行文字,firstTextBaseline 為第一行文字基線,lastTextBaseline 為最後一行文字基線

SwiftUI 對於佈局容器( 複合檢視 )的 firstTextBaseline 和 lastTextBaseline 的不透明計算方法,是產生困惑的主要原因。

swift Button("Hello world"){} .buttonStyle(.borderedProminent) .controlSize(.large)

image-20220629212234572

swift Button(action: {}, label: { Capsule(style: .circular).fill(.yellow.gradient).frame(width: 30, height: 15) }) .buttonStyle(.borderedProminent) .controlSize(.large)

image-20220630112907178

swift Text("Hello world") .frame(width: 100, height: 100, alignment: .topLeading) .border(.red)

image-20220629210927483

swift VStack { Rectangle().fill(.red.gradient).frame(width: 50, height: 10) Text("Hello world") Text("WWDC 2022") Text("肘子的 Swift 記事本") Rectangle().fill(.blue.gradient).frame(width: 50, height: 10) } .border(.red)

image-20220630112242594

swift VStack { Rectangle().fill(.red.gradient).frame(width: 50, height: 50) Rectangle().fill(.blue.gradient).frame(width: 50, height: 50) } .border(.red)

image-20220630112428784

swift HStack(alignment: .center) { Rectangle().fill(.blue.gradient).frame(width: 20, height: 50) Text("Hello world") .frame(width: 100, height: 100, alignment: .top) Text("山不在高,有仙則名。水不在深,有龍則靈。斯是陋室,惟吾德馨。苔痕上階綠,草色入簾青。談笑有鴻儒,往來無白丁。可以調素琴,閱金經。無絲竹之亂耳,無案牘之勞形。南陽諸葛廬,西蜀子云亭。孔子云:何陋之有?") .frame(width: 100) Text("WWDC 2022") .frame(width: 100, height: 100, alignment: .center) Rectangle().fill(.blue.gradient).frame(width: 20, height: 50) } .border(.red)

image-20220630113215811

swift ZStack { Text("Hello world") .frame(width: 100, height: 100, alignment: .topTrailing) .border(.red) Color.blue.opacity(0.2) Text("肘子的 Swift 記事本") .frame(width: 100, height: 100, alignment: .bottomLeading) .border(.red) } .frame(width: 130, height: 130) .border(.red)

image-20220629211312570

swift Grid { GridRow(alignment:.lastTextBaseline) { Text("Good") Text("Hello world") .frame(width: 50, height:50, alignment: .top) .border(.red) Text("Nice") } GridRow { Color.red.opacity(0.3) Color.green.opacity(0.2) Color.pink.opacity(0.2) } GridRow(alignment:.top) { Text("Start") Text("WWDC 2022") .frame(width: 70, height:50, alignment: .center) .border(.red) Rectangle() .fill(.blue.gradient) } } .frame(maxWidth: 300, maxHeight: 300) .border(.red)

image-20220630113419551

swift HStack { Text("First") VStack { Text("Hello world") Text("肘子的 Swift 記事本") Text("WWDC") } .border(.red) .padding() Text("Second") Rectangle().fill(.red.gradient) .frame(maxWidth: 10, maxHeight: 100) } .border(.green)

image-20220630113655186

請暫停閱讀下文,看看你是否可以從上面的程式碼中總結出 SwiftUI 對於佈局容器( 複合檢視 )的 firstTextBaseline 和 lastTextBaseline 的計算規律。

複合檢視的 firstTextBaseline 和 lastTextBaseline 計算方法為:

  • 對於 firstTextBaseline ,如果複合檢視中( 容器中 )的子檢視存在顯式值非 nil 的 firstTextBaseline ,則返回顯式值位置最高的 firstTextBaseline,否則返回預設值( 通常為 bottom )
  • 對於 lastTextBaseline ,如果複合檢視中( 容器中 )的子檢視存在顯式值非 nil 的 lastTextBaseline ,則返回顯式值位置最低的 lastTextBaseline,否則返回預設值( 通常為 bottom )

這就是儘管開發者很少會在 alignmentGuide 中關心並使用對齊指南的顯式值,但它在 SwiftUI 中仍十分重要的原因。

為符合 Layout 協議的自定義佈局設定顯式對齊指南

SwiftUI 4.0 新增的 Layout 協議,讓開發者擁有了自定義佈局容器的能力。通過使用 Layout 協議提供的 explicitAlignment 方法,我們可以驗證上面有關佈局容器( 複合檢視 )的 firstTextBaseline 和 lastTextBaseline 的演算法正確與否。

Layout 協議提供了兩個不同引數型別的 explicitAlignment 方法,分別對應 VerticalAlignment 和 HorizontalAlignment 型別。explicitAlignment 讓開發者可以站在佈局的角度來設定對齊指南的顯式值。explicitAlignment 的預設實現將為任何的佈局指南的顯式值返回 nil 。

下面的程式碼片段來自本文附帶的原始碼 —— 用 Layout 協議仿製 ZStack 。我將通過在 explicitAlignment 方法中分別為 firstTextBaseline 和 lastTextBaseline 設定了顯式對齊指南,以證實之前的猜想。

```swift // SwiftUI 通過此方法來獲取特定的對齊指南的顯式值 func explicitAlignment(of guide: VerticalAlignment, // 查詢的對齊指南 in bounds: CGRect, // 自定義容器的 bounds ,該 bounds 的尺寸由 sizeThatFits 方法計算得出,與 placeSubviews 的 bounds 引數一致 proposal: ProposedViewSize, // 父檢視的推薦尺寸 subviews: Subviews, // 容器內的子檢視代理 cache: inout CacheInfo // 快取資料,本例中,我們在快取資料中儲存了每個子檢視的 viewDimension、虛擬 bounds 能資訊 ) -> CGFloat? { let offsetY = cache.cropBounds.minY * -1 let infinity: CGFloat = .infinity

// 檢查子檢視中是否有 顯式 firstTextBaseline 不為 nil 的檢視。如果有,則返回位置最高的 firstTextBaseline 值。 
if guide == .firstTextBaseline,!cache.subviewInfo.isEmpty {
    let firstTextBaseline = cache.subviewInfo.reduce(infinity) { current, info in
        let baseline = info.viewDimension[explicit: .firstTextBaseline] ?? infinity
        // 將子檢視的顯式 firstTextBaseline 轉換成 bounds 中的偏移值
        let transformBaseline = transformPoint(original: baseline + info.bounds.minY, offset: offsetY, targetBoundsMinX: 0)
        // 返回位置最高的值( 值最小 )
        return min(current, transformBaseline)
    }
    return firstTextBaseline != infinity ? firstTextBaseline : nil
}

if guide == .lastTextBaseline,!cache.subviewInfo.isEmpty {
    let lastTextBaseline = cache.subviewInfo.reduce(-infinity) { current, info in
        let baseline = info.viewDimension[explicit: .lastTextBaseline] ?? -infinity
        let transformBaseline = transformPoint(original: baseline + info.bounds.minY, offset: offsetY, targetBoundsMinX: 0)
        return max(current, transformBaseline)
    }
    return lastTextBaseline != -infinity ? lastTextBaseline : nil
}

return nil

} ```

由於檢視使用 Layout 協議的 explicitAlignment 方法的預設實現效果與使用我們自定義的方法效果完全一致,因此可以證明我們之前的猜想是正確的。如果你只想讓你的自定義佈局容器呈現與 SwiftUI 預置容器一致的對齊指南效果,直接使用 Layout 協議的預設實現即可( 無需實現 explicitAlignment 方法 )。

即使佈局容器通過 explicitAlignment 為對齊指南提供了顯式值,開發者仍然可以通過 alignmentGuide 做進一步設定。

對哪些檢視進行“對齊”

在上文中我們用了不小的篇幅介紹了對齊指南,本節中我們將探討“對齊”的另一大關鍵點 —— 在不同的上下文中,哪些檢視會使用對齊指南進行“對齊”。

VStack、HStack、ZStack 等支援多檢視的佈局容器

你是否瞭解 SwiftUI 常用佈局容器構造方法中的對齊引數的含義?它們又是如何實現的呢?

swift VStack(alignment:.trailing) { ... } ZStack(alignment: .center) { ... } HStack(alignment:.lastTextBaseline) { ... } GridRow(alignment:.firstTextBaseline) { ... }

由於蘋果對容器檢視的 alignment 引數的描述並不很清晰,因而開發者很容易出現理解偏差。

The guide for aligning the subviews in this stack. This guide has the same vertical screen coordinate for every child view —— Apple documentation for VStack's alignment

對於本段檢視宣告程式碼,你會選擇下面哪種文字表述:

swift ZStack(alignment: .bottomLeading) { Rectangle() .fill(.orange.gradient) .frame(width: 100, height: 300) Rectangle() .fill(.cyan.gradient).opacity(0.7) .frame(width: 300, height: 100) }

  1. 在 ZStack 中按順序重疊排列子檢視( Rectangle1 和 Rectangle2 ),並讓每個子檢視的 bottomLeading 與 ZStack 的 bottomLeading 對齊

  2. 按順序重疊排列 Rectangle1 和 Rectangle2,並讓兩者的 bottomLeading 對齊

image-20220701132738722

如果你選擇了 1 ,請問你該如何解釋下面程式碼中的 alignmentGuide 無法影響子檢視的對齊。

swift ZStack(alignment: .bottomLeading) { Rectangle() .fill(.orange.gradient) .frame(width: 100, height: 300) Rectangle() .fill(.cyan.gradient).opacity(0.7) .frame(width: 300, height: 100) } .alignmentGuide(.leading){ $0[.leading] + 10 }

描述 1 在絕大多數的情況下( 不設定對齊指南顯式值 )看起來都像是正確的,而且也很符合人的直覺,但從 SwiftUI 的角度來說,它將根據描述二來執行。因為在佈局容器構造方法中設定的對齊指南只用於容器的子檢視之間。

為了更好地理解之所以描述二才是正確的,我們需要對 SwiftUI 的佈局原理以及 ZStack 的處理方式有所瞭解。

佈局容器在佈局時,容器會為每個子檢視提供一個建議尺寸( proposal size ),子檢視將參考容器提供的建議尺寸返回自己的需求尺寸( 子檢視也可以完全無視容器的建議尺寸而提供任意的需求尺寸 )。容器按照預設的行為( 在指定軸向排列、點對齊、線對齊 、新增間隙等 )在一個虛擬的畫布中擺放所有的子檢視。擺放結束後,容器將彙總擺放後的所有子檢視的情況並向它的父檢視( 父容器 )返回一個自身的需求尺寸。

因此,在佈局容器對子檢視進行對齊擺放過程中,佈局容器的尺寸並沒有確定下來,所以不會存在將子檢視的對齊指南與容器的對齊指南進行“對齊”的可能。

通過建立符合 Layout 協議的佈局容器可以清楚地展示上述的過程,下面的程式碼來自本文附帶的演示程式碼 —— 一個 ZStack 的複製品 :

```swift // 容器的父檢視(父容器)通過呼叫容器的 sizeThatFits 獲取容器的理想尺寸,本方法通常會被多次呼叫,並提供不同的建議尺寸 func sizeThatFits( proposal: ProposedViewSize, // 容器的父檢視(父容器)提供的建議尺寸 subviews: Subviews, // 當前容器內的所有子檢視的代理 cache: inout CacheInfo // 快取資料,本例中用於儲存子檢視的返回的需求尺寸,減少呼叫次數 ) -> CGSize { cache = .init() // 清除快取 for subview in subviews { // 為子檢視提供建議尺寸,獲取子檢視的需求尺寸 (ViewDimensions) let viewDimension = subview.dimensions(in: proposal) // 根據 MyZStack 的 alignment 的設定獲取子檢視 alignmentGuide 對應的點 let alignmentGuide: CGPoint = .init( x: viewDimension[alignment.horizontal], y: viewDimension[alignment.vertical] ) // 以子檢視的 alignmentGuide 對應點為 (0,0) , 在虛擬的畫布中,為子檢視建立 Bounds let bounds: CGRect = .init( origin: .init(x: -alignmentGuide.x, y: -alignmentGuide.y), size: .init(width: viewDimension.width, height: viewDimension.height) ) // 儲存子檢視在虛擬畫布中的資訊 cache.subviewInfo.append(.init(viewDimension: viewDimension, bounds: bounds)) }

// 根據所有子檢視在虛擬畫布中的資料,生成 MyZStack 的 Bounds
cache.cropBounds = cache.subviewInfo.map(\.bounds).cropBounds()
// 返回當前容器的需求尺寸,當前容器的父檢視將使用該尺寸在它的內部進行擺放
return cache.cropBounds.size

}

// 容器的父檢視(父容器)將在需要的時機呼叫本方法,為本容器的子檢視設定渲染位置 func placeSubviews( in bounds: CGRect, // 根據當前容器在 sizeThatFits 提供的尺寸,在真實渲染處建立的 Bounds proposal: ProposedViewSize, // 容器的父檢視(父容器)提供的建議尺寸 subviews: Subviews, // 當前容器內的所有子檢視的代理 cache: inout CacheInfo // 快取資料,本例中用於儲存子檢視的返回的理想尺寸,減少呼叫次數 ) { // 虛擬畫布左上角的偏移值 ( 到 0,0 ) let offsetX = cache.cropBounds.minX * -1 let offsetY = cache.cropBounds.minY * -1

for index in subviews.indices {
    let info = cache.subviewInfo[index]
    // 將虛擬畫布中的位置資訊轉換成渲染 bounds 的位置資訊
    let x = transformPoint(original: info.bounds.minX, offset: offsetX, targetBoundsMinX: bounds.minX)
    let y = transformPoint(original: info.bounds.minY, offset: offsetY, targetBoundsMinX: bounds.minY)
    // 將轉換後的位置資訊設定到子檢視上
    subviews[index].place(at: .init(x: x, y: y), anchor: .topLeading, proposal: proposal)
}

} ```

VStack 和 HStack 相對於 ZStack 在佈局時將更加複雜。由於需要考慮在特定維度上可動態調整尺寸的子檢視,比如: Spacer 、Text 、frame(minWidth:maxWidth:minHeight:maxHeight) 等,VStack 和 HStack 會為子檢視進行多次尺寸提案( 包括理想尺寸、最小尺寸、最大尺寸、特定尺寸等 ),並結合子檢視的佈局優先順序( layoutPriority )才能計算出子檢視的需求尺寸,並最終確定自身的尺寸。

總之,為 VStack、HStack、ZStack 這類可包含多個子檢視的官方佈局容器設定 alignment 的含義就只有一種 —— 在特定維度上,將所有的子檢視按照給定的對齊指南進行對齊擺放。

overlay、background

在 SwiftUI 中,除了我們熟悉的 VStack、HStack、ZStack 、Grid 、List 外,很多 modifier 的功能也都是通過佈局來實現的。例如 overlay、background、frame、padding 等等。

你可以將 overlay 和 background 視作一個特殊版本的 ZStack 。

swift // 主檢視 Rectangle() .fill(.orange.gradient) .frame(width: 100, height: 100) // 附加檢視 .overlay(alignment:.topTrailing){ Text("Hi") }

比如上面的程式碼,如果用佈局的邏輯可以表示為( 虛擬碼):

```swift _OverlayLayout { // 主檢視 Rectangle() .fill(.orange.gradient) .frame(width: 100, height: 100)

// 附加檢視
Text("Hi")
    .layoutValue(key: Alignment.self, value: .topTrailing) // 一種子檢視向最近容器傳遞資訊的方式

} ```

與 ZStack 的不同在於,它只會包含兩個子檢視,且它的尺寸將僅由主檢視來決定。主檢視將和附加檢視按照設定的對齊指南進行對齊。只要理解了這點,就會知道該如何調整主檢視或輔助檢視的對齊指南了,比如:

swift // 主檢視 Rectangle() .fill(.orange.gradient) .frame(width: 100, height: 100) .alignmentGuide(.trailing, computeValue: { $0[.trailing] - 30 }) .alignmentGuide(.top, computeValue: { $0[.top] + 30 }) // 附加檢視 .overlay(alignment:.topTrailing){ Text("Hi") }

image-20220701143710982

frame

frame 本質上就是 SwiftUI 中一個用於調節尺寸的佈局容器,它會變換容器傳遞給子檢視的建議尺寸,也可能會改變子檢視返回給容器的需求尺寸。比如:

swift VStack { Text("Hello world") .frame(width: 10, height: 30, alignment: .top) }

在上面的程式碼中,由於添加了 frame 修飾器,因此 FrameLayout( 實現 frame 的後端佈局容器 )將無視 VStack 提供的建議尺寸,強行為 Text 提供 10 x 30 的建議尺寸,並且無視子檢視 Text 的需求尺寸,為父檢視( VStack )返回 10 x 30 的需求尺寸。雖然 FrameLayout 中只包含一個子檢視,但在佈局時它會讓子檢視與一個特定尺寸的虛擬檢視進行對齊。或許將上面的 frame 程式碼轉換成 background 的佈局模式會更加方便理解:

```swift _BackgroundLayout { Color.clear .frame(width: 10, height: 30)

Text("Hello world")
    .layoutValue(key: Alignment.self, value: .top)

} ```

動態版本的 frame( FlexFrameLayout ) 修飾器是一個學習、理解 SwiftUI 佈局中尺寸協商機制的絕佳案例。有興趣的朋友可以使用 Layout 協議對其進行仿製。

總結

雖然本文並沒有提供具體的對齊使用技巧,但只要你理解並掌握了對齊的兩大要點:以什麼為對齊指南、對哪些檢視進行“對齊”,那麼相信一定會減少你在開發中遇到的對齊困擾,並可以通過對齊實現很多以前不容易完成的效果。

如果你想對 Layout 協議做更全面地瞭解,推薦你觀看 ChaoCode( 美眉 up 主)製作的有關 SwiftUI Layout 協議的中文視訊 —— 自訂 Layout 排版教學

希望本文能夠對你有所幫助。

原文發表在我的部落格 wwww.fatbobman.com

歡迎訂閱我的公共號:【肘子的Swift記事本】