在 SwiftUI 中實現檢視居中的若干種方法
highlight: a11y-dark
將某個檢視在父檢視中居中顯示是一個常見的需求,即使對於 SwiftUI 的初學者來說這也並非難事。在 SwiftUI 中,有很多手段可以達成此目的。本文將介紹其中的一些方法,並對每種方法背後的實現原理、適用場景以及注意事項做以說明。
原文發表在我的部落格 wwww.fatbobman.com
歡迎訂閱我的公共號:【肘子的Swift記事本】
需求
實現下圖中展示的樣式:在彩色矩形檢視中居中顯示單行 Text
填充物
Spacer
最常見也是最容易想到的解決方案。
```swift var hello: some View { Text("Hello world") .foregroundColor(.white) .font(.title) .lineLimit(1) }
HStack { Spacer() hello Spacer() } .frame(width: 300, height: 60) .background(.blue) ```
如果我告訴你上面的程式碼有兩個隱患你相信嗎?
- 文字內容超出了矩形的寬度
Spacer 是有最小厚度設定的,預設的最小墊片厚度為 8px 。即使文字寬度超出了 HStack 給出的建議寬度,但 HStack 在佈局時,仍會保留其最小厚度,導致下圖上方的文字無法充分利用矩形檢視的寬度。
解決方法為:Spacer(minLength: 0)
。
當然,你也可以利用 Spacer 這個特性,控制 Text 在 HStack 中可使用的寬度。
- 將合成後的檢視放置在某個可能會充滿螢幕的檢視的頂部或底部顯示結果或者與你的預期不符
```swift VStack { // Hello world 檢視 1 HStack { Spacer(minLength: 0) hello Spacer(minLength: 0) } .frame(width: 300, height: 60) .background(.blue)
HStack {
Spacer(minLength: 0)
hello
Spacer(minLength: 0)
}
.frame(width: 300, height: 60) // 相同的尺寸
.background(.red)
Spacer() // 讓 VStack 充滿可用空間
} ```
從 SwiftUI 3.0 開始,在使用 background 新增符合 ShapeStyle 協議的元素時,可以通過 ignoresSafeAreaEdges 引數設定是否忽略安全區域,預設值為 .all
( 忽略任何的安全區域 )。因此,當我們將合成後的 hello world 檢視放置在 VStack 頂部時( 通過 Spacer ),矩形的 background 會連同頂部的安全區域一併渲染。
解決的方法是:.background(.blue, ignoresSafeAreaEdges: [])
,排除掉不希望忽略的安全區域。
另外,在給定尺寸不明的情況下( 未顯式為矩形設定尺寸 ),上面的程式碼也需要進行一定的調整。例如,在 List Row 中顯示 hello world 檢視,希望矩形能夠充滿 Row :
```swift List { HStack { Spacer(minLength: 0) hello Spacer(minLength: 0) } .background(.blue) .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) // 將 Row 的 Insets 設定為 0
} .listStyle(.plain) .environment(.defaultMinListRowHeight, 80) // 設定 List 最小行高度
```
hello world 檢視並不能充滿 Row 提供的高度。這是由於 HStack 的高度是由容器子檢視對齊排列後的高度決定的。Spacer 在 HStack 中只能進行橫向填充,並不具備縱向的高度( 高度為 0 ),因此 HStack 最終的需求高度與 Text 的高度一致。
解決的方法是:
swift
HStack {
Spacer(minLength: 0)
hello
Spacer(minLength: 0)
}
.frame(maxHeight: .infinity) // 用滿建議高度
.background(.blue)
後文中為了簡潔將省略掉針對給定尺寸不明情況的處理方式。統一使用固定尺寸(
.frame(width: 300, height: 60)
)。
其他填充物
那麼,我們是否可以利用其它的檢視實現與 Spacer 類似的填充效果呢?例如:
```swift HStack { Color.clear hello Color.clear } .frame(width: 300, height: 60) .background(Color.cyan)
```
很遺憾,使用上面的程式碼,Text 將只能使用 HStack 三分之一的寬度。
HStack、VStack 在進行佈局時,會為每個子檢視提供四種不同的建議模式( 最小、最大、明確尺寸以及未指定 ),如果子檢視在不同的模式下返回的需求尺寸是不一樣的,則意味著該檢視是可變尺寸檢視。那麼 HStack、VStack 會在明確了所有固定尺寸子檢視的需求尺寸後,將所剩的可用尺寸( HStack、VStack 的父檢視給他們的建議尺寸 - 固定尺寸子檢視的需求尺寸 )平均分配( 在優先順序相同的情況下 )給這些可變尺寸檢視。
由於 Color、Text 都具備可變尺寸的特性,因此,它們三等分了 HStack。
但是我們可以通過調整檢視優先順序的方式,來保證 Text 能夠獲得最大的分量,例如:
```swift HStack { Color.clear .layoutPriority(0) hello .layoutPriority(1) Color.clear .layoutPriority(0) } .frame(width: 300, height: 60) .background(Color.cyan)
Text("Hello world,hello world,hello world") // hello 的寬度超出了矩形的寬度 ```
至於上圖中 Text 仍沒有充分利用 HStack 全部寬度的原因,是因為沒有為 HStack 設定明確的 spacing ,將其設定為 0 即可:HStack(spacing:0)
。
為佈局容器設定明確的 spacing 是一個好習慣,在未明確指定時,HStack、VStack 在進行佈局時可能會出現某些異常。下文中也會碰到此種情況。
HStack、VStack 是不會給 Spacer 分配 spacing 的,畢竟 Spacer 本身就代表了空間佔用。因此在第一個例子中,即使沒有為 HStack 設定 spacing ,Text 仍然會使用全部的 HStack 寬度。
掌握了檢視優先順序的使用方式,我們還可以利用其他具備可變尺寸的特性的檢視來充當填充物,例如:
Rectangle().opacity(0)
Color.blue.opacity(0)
ContainerRelativeShape().fill(.clear)
在使用 SwiftUI 進行開發的過程中,Color、Rectangle 等經常被用來實現對容器的等分操作。另外,由於 Color、Rectangle 會在兩個維度進行填充( Spacer 會根據容器選擇填充維度 ),因此,使用它們作為填充物時,將會自動使用全部的可用空間( 包括高度 ),無需通過 .frame(maxHeight: .infinity)
應對給定尺寸不明的場景。
請閱讀 SwiftUI 專欄 #4 Color 不只是顏色 ,掌握有關 Color 更多的內容
對齊指南
上節中,我們通過填充物讓 Text 實現了左右居中。上下居中則是利用了 HStack 對齊指南的預設設定( .center
)實現的。本節中,我們將完全通過對齊指南來實現居中操作。
ZStack
swift
ZStack { // 使用對齊指南的預設值,相當於 ZStack(alignment:.center)
Color.green
hello
}
.frame(width: 300, height: 60)
上述程式碼的佈局邏輯是:
- ZStack 為 Color 和 Text 分別給出了 300 x 60 的建議尺寸
- Color 會將建議尺寸作為自己的需求尺寸( 表現為充滿 ZStack 空間 )
- Text 最大可用寬度為 300
- Color 與 Text 將按照對齊指南 center 進行對齊( 看起來就是 Text 顯示在 Color 的中間 )
如果將程式碼改寫成下面的方式就會出現問題:
swift
ZStack { // 在不明確設定 VStack spacing 的情況下,會出現 VStack spacing 不一致的情況
Color.gray
.frame(width: 300, height: 60)
hello // 寬度沒有約定,當文字較長時,會超過 Color 的寬度
}
上方程式碼的佈局邏輯是:
- Color 的尺寸為 300 x 60 ( 不關心 ZStack 給出的建議尺寸 )
- ZStack 的尺寸為 Color 和 Text 兩者的最大寬度 x 最大高度,該尺寸是一個可變尺寸( 取決於 Text 文字的長度 )
- 當 ZStack 給出的建議寬度大於 300 時,Text 的可利用寬度將超過 Color 的寬度
因此會出現兩種可能的錯誤狀態:
- 當文字較長時,Text 會超過 Color 的寬度
- 由於合成檢視具備可變尺寸特性,VStack、HStack 在為其新增 spacing 時將可能出現異常 ( 下圖中 spacing 的分配不均勻。顯式設定可以解決該問題,請養成顯式設定 spacing 的習慣 )
```swift VStack { // 沒有設定 spacing ,顯式設定可修復 spacing 不均勻的問題 ZStack { Color.green hello } .frame(width: 300, height: 60)
ZStack { // 在不明確設定 VStack spacing 的情況下,會出現 VStack spacing 不一致的情況
Color.gray
.frame(width: 300, height: 60)
hello // 對於文字超過矩形寬度的情況不好處理
}
// Spacer 版本
HStack {
Spacer(minLength: 0)
hello
.sizeInfo()
Spacer(minLength: 0)
.sizeInfo()
}
.frame(width: 300, height: 60)
.background(.blue, ignoresSafeAreaEdges: [])
} ```
frame
swift
hello
.frame(width: 300, height: 60) // 使用了預設的 center 的對齊指南,相當於 .frame(width: 300, height: 60,alignment: .center)
.background(.pink)
佈局邏輯:
- 使用 FrameLayout 佈局容器對 Text 進行佈局
- FrameLayout 給 Text 的建議尺寸為 300 x 60
- Text 與佔位檢視( 空白檢視的尺寸為 300 x 600 )按對齊指南 center 進行對齊
這是我個人最喜歡使用的居中手段,應對給定尺寸不明的情況也十分方便:
swift
hello
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.pink)
想了解 frame 的實現原理請閱讀 SwiftUI 佈局 —— 尺寸( 下 ) 一文
overlay
swift
Rectangle() // 直接使用 Color.orange 也可以
.fill(Color.orange)
.frame(width: 300, height: 60)
.overlay(hello) // 相當於 .overlay(hello,alignment: .center)
佈局邏輯:
- Rectangle 將獲得 300 x 60 建議尺寸( Rectangle 將使用全部的尺寸 )
- 使用 OverlayLayout 佈局容器對 Rectangle 及 Text 進行佈局,建議尺寸採用主檢視的需求尺寸( Rectangle 的需求尺寸 )
- Text 與 Rectangle 按照對齊指南 center 進行對齊
那麼是否可以用 background 實現類似的樣式呢?例如:
swift
hello
.background(
Color.cyan.frame(width: 300,height: 60)
)
.border(.red) // 顯示邊框以檢視合成檢視的佈局尺寸
很遺憾,你將獲得與上文中 ZStack 錯誤用法類似的結果。文字可能會超長,檢視無法獲得 spacing ( 即使進行了顯式設定 )。
請閱讀 SwiftUI 佈局 —— 對齊 ,瞭解更多有關 ZStack、overlay、background 的對齊機制
Geometry
雖然有些大材小用,但當我們需要獲取更多有關檢視的資訊時,GeometryReader 是一個相當不錯的選擇:
swift
GeometryReader { proxy in
hello
.position(.init(x: proxy.size.width / 2, y: proxy.size.height / 2))
.background(Color.brown)
}
.frame(width: 300, height: 60)
佈局邏輯:
- GeometryReader 將獲得 300 x 60 的建議尺寸
- 由於 GeometryReader 擁有與 Color、Rectangle 類似的特徵,會將給定的建議尺寸作為需求尺寸( 表現為佔用全部可用空間 )
- GeometryReader 給 Text 提供 300 x 60 的建議尺寸
- GeometryReader 中的檢視,預設基於 topLeading 對齊( 類似
overlay(alignment:.topLeading)
的效果 ) - 使用 postion 將 Text 的中心點與給定的位置進行對齊( postion 是一個通過 CGPoint 來對齊中心點的檢視修飾器 )
當然,你也可以獲取 Text 的 Geometry 資訊,通過 offset 或 padding 的方式實現居中。不過除非矩形的尺寸明確,否則裡外都需要使用 GeometryReader ,實現將過於煩瑣。
總結
本文選取了一些有代表性的解決方法,隨著 SwiftUI 功能的不斷增強,會有越來越多的手段可供使用。萬變不離其宗,掌握了 SwiftUI 的佈局原理,無論需求如何變化都可輕鬆應對。
我為本文這種通過多種方法來解決一個問題的方式添加了【小題大做】標籤,目前使用該便籤的文章還有:在 Core Data 中查詢和使用 count 的若干方法、在 SwiftUI 檢視中開啟 URL 的若干方法 。
希望本文能夠對你有所幫助。同時也歡迎你通過 Twitter、 Discord 頻道或下方的留言板與我進行交流。
我正以聊天室、Twitter、部落格留言等討論為靈感,從中選取有代表性的問題和技巧製作成 Tips ,釋出在 Twitter 上。每週也會對當周部落格上的新文章以及在 Twitter 上釋出的 Tips 進行彙總,並通過郵件列表的形式傳送給訂閱者。
訂閱下方的 郵件列表,可以及時獲得每週的 Tips 彙總。
原文發表在我的部落格 wwww.fatbobman.com
歡迎訂閱我的公共號:【肘子的Swift記事本】
- 自定義 Button 的外觀和互動行為
- MacBook Pro 使用體驗
- 用 SwiftUI 的方式進行佈局
- 聊一聊可組裝框架( TCA )
- StateObject 與 ObservedObject
- 一些適合 SwiftUI 初學者的教程
- iBug 16 有感
- 在 SwiftUI 中實現檢視居中的若干種方法
- SwiftUI 佈局 —— 尺寸( 下 )
- SwiftUI 佈局 —— 尺寸( 上 )
- SwiftUI 佈局 —— 對齊
- Core Data with CloudKit(三)——CloudKit儀表臺
- Core Data with CloudKit(二)——同步本地資料庫到iCloud私有資料庫
- 在CoreData中使用持久化歷史跟蹤
- 用 Table 在 SwiftUI 下建立表格
- SwiftUI 4.0 的全新導航系統
- 如何在 Core Data 中進行批量操作
- Core Data 是如何在 SQLite 中儲存資料的
- 在 SwiftUI 檢視中開啟 URL 的若干方法
- 為自定義屬性包裝型別新增類 @Published 的能力