本質(zhì)上会前,Alignment Guides屬于SwiftUI中布局的知識點特碳,在某些特殊場景下话浇,使用Alignment Guides能起到事半功倍的效果,比如我們平時經(jīng)常用的下邊的這樣的效果:
上圖顯示了疚颊,當切換背景容器的最大寬度時狈孔,使用Alignment Guides能夠自動執(zhí)行動畫效果,這正是我們想要的材义,核心代碼如下:
struct TestWrappedLayout: View {
let w: CGFloat
var texts: [String]
var body: some View {
self.generateContent(in: w)
}
private func generateContent(in w: CGFloat) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(self.texts, id: \.self) { t in
self.item(for: t)
.padding([.trailing, .bottom], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > w)
{
width = 0
height -= d.height
}
let result = width
if t == self.texts.last! {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if t == self.texts.last! {
height = 0 // last item
}
return result
})
}
}
}
func item(for text: String) -> some View {
Text(text)
.padding([.leading, .trailing], 8)
.frame(height: 30)
.font(.subheadline)
.background(Color.orange)
.foregroundColor(.white)
.cornerRadius(15)
.onTapGesture {
print("你好啊")
}
}
}
在本篇文章中均抽,最核心的思想就是容器container中的每個View都有它的alignment guide。
Alignment Guide是什么母截?
說到對齊到忽,大家頭腦中一定要有一個組的概念,也就是group清寇,如果只有一個view喘漏,那對齊就失去了意義,我們在設計對齊相關的ui的時候华烟,是對一組中的多個view進行考慮的翩迈,這也恰恰和容器的概念對應上了,我這里說的容器指的是VStack
,HStack
,ZStack
盔夜。
換句話說负饲,我們對容器內(nèi)的Views使用Alignment guide。
對齊共分為兩種:水平對齊(horizontal),垂直對齊(vertical)
我們先以水平對齊為例喂链,先看下圖:
有3個view返十,分別為A,B椭微,C洞坑,他們alignment guide返回的值分別為0, 20蝇率, 10迟杂,從上圖可以看出,他們的偏移關系正好和值對應上了本慕,當值為正的時候排拷,往左偏移,為負的時候锅尘,往右偏移监氢。這里有下邊幾個概念,大家一定要理解鉴象,如果不理解這幾個概念忙菠,就無法真正明白對齊的奧義:
- 我們把A,B纺弊,C放到了VStack中牛欢,VStack中使用的對齊方式是水平對齊,比如VStack(alignment: .center)`
- alignment guide返回的值表達的是這3個view的位置關系淆游,并不是說A的返回值為0傍睹,A就不偏移隔盛,我們需要把他們作為一個整體來看,通過偏移量來描述他們之間的位置關系拾稳,然后讓他們3個view在VStack中整體居中
上邊的重點是吮炕,alignment guide描述的是views之間的位置關系,系統(tǒng)在布局的時候访得,會把他們看成一個整體龙亲,然后在使用frame alignment guide對整體進行布局。
同樣的道理悍抑,下邊圖片展示的是垂直對齊鳄炉,我們就不再多做解釋了:
通過上邊這兩個例子,我們得出一個結論:VStack需要水平對齊搜骡,HStack需要垂直對齊拂盯,雖然這聽上去有點怪,但只需在頭腦中想一想他們中view的排列方式记靡,就不難理解谈竿。至于ZStack,即需要水平對齊摸吠,也需要垂直對齊空凸,這個我們在下邊的小節(jié)中,詳細解釋寸痢。
Alignment Guide中的疑惑
相信大家在代碼中的很多地方會用到.leading
劫恒,在SwiftUI中,用到對齊的地方一共有下邊幾種:
這張圖片覆蓋了對齊所有的使用方式轿腺,現(xiàn)在大家可能是一臉茫然,但讀完剩下的文章后丛楚,再回過頭來看這張圖片族壳,就會發(fā)現(xiàn),這張圖片實在是太經(jīng)典了趣些,毫不夸張的說仿荆,你以后在SwiftUI中使用alignment guide的時候,頭腦中一定會浮現(xiàn)出這張圖片坏平。
我們對上邊的幾個概念做個簡單的介紹:
-
Container Alignment: 容器的對齊方式主要有2個目的拢操,首先它定義了其內(nèi)部views的隱式對齊方式,沒有使用
alignmentGuides()
modifier的view都使用隱式對齊舶替,然后定義了內(nèi)部views中使用了alignmentGuides()
的view令境,只有參數(shù)與容器對齊參數(shù)相同,容器才會根據(jù)返回值計算布局 - Alignment Guide:如果該值和Container Alignment的參數(shù)不匹配顾瞪,則不會生效
- Implicit Alignment Value:通常來說舔庶,隱式對齊采用的值都是默認的值抛蚁,系統(tǒng)通常會使用和對齊參數(shù)相匹配的值
- Explicit Alignment Value:顯式對齊跟隱式對齊相反,是我們自己用程序明確給出的返回值
- Frame Alignment:表示容器中views的對齊方式惕橙,把views看作一個整體瞧甩,整體偏左,居中弥鹦,或居右
- Text Alignment:控制多行文本的對齊方式
隱式和顯式對齊的區(qū)別
每個view都有一個alignment肚逸,記住這一點非常重要,當我們使用.alignmentGuide()
設置對齊方式時彬坏,我們稱之為顯式朦促,相反則稱之為隱式。隱式的情況下苍鲜,.alignmentGuide()
的返回值和它父類容器的對齊參數(shù)有關思灰。
如果我們沒有為VStack
, HStack
和 ZStack
提供alignment參數(shù),默認值為center混滔。
ViewDimensions
func alignmentGuide(_ g: HorizontalAlignment, computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View
func alignmentGuide(_ g: VerticalAlignment, computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View
我們已經(jīng)知道了computeValue函數(shù)的返回值是一個CGFloat
類型洒疚,但我們不太清楚ViewDimensions
是個什么東西?很簡單坯屿,我們可以直接查看它的系統(tǒng)定義:
public struct ViewDimensions {
public var width: CGFloat { get } // The view's width
public var height: CGFloat { get } // The view's height
public subscript(guide: HorizontalAlignment) -> CGFloat { get }
public subscript(guide: VerticalAlignment) -> CGFloat { get }
public subscript(explicit guide: HorizontalAlignment) -> CGFloat? { get }
public subscript(explicit guide: VerticalAlignment) -> CGFloat? { get }
}
很容易發(fā)現(xiàn)油湖,通過width
和height
,我們很容易獲得該view的寬和高,這在我們返回對齊值的時候非常有用领跛,我們不做過多解釋乏德,我們往下看,subscript
表明我們可以像這樣訪問:d[HorizontalAlignment.leading]
吠昭。
那么這有什么用呢喊括? 我們先看段代碼:
struct Example6: View {
var body: some View {
ZStack(alignment: .topLeading) {
Text("Hello")
.alignmentGuide(HorizontalAlignment.leading, computeValue: { d in return 0 })
.alignmentGuide(.top, computeValue: { d in return 0 })
.background(Color.green)
Text("World")
.alignmentGuide(.top, computeValue: { d in return 100 })
.alignmentGuide(HorizontalAlignment.leading, computeValue: { d in return 0 })
.background(Color.purple)
}
.background(Color.orange)
}
}
這段代碼運行后的效果
由于我們給Text("World")
設置了.alignmentGuide(.top, computeValue: { d in return 100 })
,因此矢棚,它出現(xiàn)在hello的上邊沒什么問題郑什,那么我如果把.alignmentGuide(HorizontalAlignment.leading, computeValue: { d in return 0 })
改成.alignmentGuide(HorizontalAlignment.leading, computeValue: { d in return d[.top] })
呢?
在設置leading對齊的時候使用了top對齊的數(shù)據(jù)蒲肋,運行效果:
可以看出蘑拯,完全符合我們的預期,world又向左偏移了100的距離兜粘,這就是我們上邊說的用法申窘,不過,通常情況下我們基本不需要這樣操作孔轴。
類似d[HorizontalAlignment.leading]
這樣的參數(shù)剃法,我們都可簡寫成d[.leading]
,Swift能夠非常智能的識別這些類型,但是center
除外路鹰,原因是HorizontalAlignment
和VerticalAlignment
都有center玄窝。
對齊類型
對于HorizontalAlignment
來說牵寺,有下邊幾個參數(shù):
extension HorizontalAlignment {
public static let leading: HorizontalAlignment
public static let center: HorizontalAlignment
public static let trailing: HorizontalAlignment
}
當我們使用下標訪問數(shù)據(jù)的時候,有兩種方式:
d[.trailing]
d[explicit: .trailing]
d[.trailing]
表示獲取d的隱式leading恩脂,也就是默認值帽氓,通常情況下,.leading
的值為0俩块,.center
的值為width的一半黎休,.trailing
的值為width。
d[explicit: .trailing]
表示獲取d的顯式的trailing玉凯,當沒有通過.alignmentGuide()
指定值的時候势腮,它返回nil,就像上一小節(jié)講的一樣漫仆,在ZStack
中捎拯,我們可以獲取顯式的對齊值
對于VerticalAlignment
來說,基本用法跟HorizontalAlignment
差不多盲厌,但它多了幾個參數(shù):
extension VerticalAlignment {
public static let top: VerticalAlignment
public static let center: VerticalAlignment
public static let bottom: VerticalAlignment
public static let firstTextBaseline: VerticalAlignment
public static let lastTextBaseline: VerticalAlignment
}
firstTextBaseline
表示所有text的以各自最上邊的那一行的base line對齊署照,lastTextBaseline
表示所有text的以最下邊的那一行的base line對齊。對于某個view而言吗浩,如果它不是多行文本建芙,則firstTextBaseline
和lastTextBaseline
是一樣的。
我們可以通過print(d[.lastTextBaseline])
打印出這些值懂扼,他們都為正值禁荸。
我們先看個firstTextBaseline
的例子:
HStack(alignment: .firstTextBaseline) {
Text("床前明月光")
.font(.caption)
.frame(width: 50)
.background(Color.orange)
Text("疑是地上霜")
.font(.body)
.frame(width: 50)
.background(Color.green)
Text("舉頭望明月")
.font(.largeTitle)
.frame(width: 50)
.background(Color.blue)
}
可以看出來,這3個text都以他們各自的第一行的base line 對齊了阀湿,我們稍微改下代碼:
HStack(alignment: .lastTextBaseline) {
...
}
他們以各自的最后一行的的base line對齊了赶熟,針對這3個text,上邊的代碼都使用了隱式的alignment guide陷嘴,那么我們再進一步嘗試钧大,我們給第3個text一個顯式的alignment guide會是怎么樣的?
HStack(alignment: .lastTextBaseline) {
Text("床前明月光")
.font(.caption)
.frame(width: 50)
.background(Color.orange)
Text("疑是地上霜")
.font(.body)
.frame(width: 50)
.background(Color.green)
Text("舉頭望明月")
.font(.largeTitle)
.alignmentGuide(.lastTextBaseline, computeValue: { (d) -> CGFloat in
d[.firstTextBaseline]
})
.frame(width: 50)
.background(Color.blue)
}
重點來了罩旋,對齊描述的是容器內(nèi)view之間的布局關系,由于computeValue函數(shù)的返回值都是CGFloat眶诈,因此不管是哪種對齊方式涨醋,最終都是得到一個CGFloat。
那么如果我們在text中間加入一個其他的view呢逝撬?
HStack(alignment: .firstTextBaseline) {
Text("床前明月光")
.font(.caption)
.frame(width: 50)
.background(Color.orange)
RoundedRectangle(cornerRadius: 3)
.foregroundColor(.green)
.frame(width: 50, height: 40)
Text("疑是地上霜")
.font(.body)
.frame(width: 50)
.background(Color.green)
Text("舉頭望明月")
.font(.largeTitle)
.alignmentGuide(.firstTextBaseline, computeValue: { (d) -> CGFloat in
return 0
})
.frame(width: 50)
.background(Color.blue)
}
- 除了text之外的其他view浴骂,都使用bottom對齊方式
- 不管是
lastTextBaseline
還是firstTextBaseline
,布局的算法都是.top + computeValue
,也就是說以它的頂部為布局的基線 - alignment 描述的是view之間的關系,把他們作為一個整體或者一組來看待
這一塊可能有點繞宪潮,但并不難理解溯警,如果大家有問題趣苏,可以留言。
我們知道HStack
使用VerticalAlignment
梯轻,VStack
使用HorizontalAlignment
食磕,他們只需要一種就行了,但是ZStack
同時需要兩種對齊方式喳挑,該如何呢彬伦?
這里引入Alignment
類型,用法如下:
ZStack(alignment: Alignment(horizontal: .leading, vertical: .top)) { ... }
本質(zhì)上伊诵,它把horizontal和vertical封裝在了一起单绑,我們平時經(jīng)常用的是下邊這個寫法,只是寫法不同而已:
ZStack(alignment: .topLeading) { ... }
Container Alignment
所謂的容器的對齊方式指的是下邊這里:
VStack(alignment: .leading)
HStack(alignment: .top)
ZStack(alignment: .topLeading)
那么它主要有什么作用呢曹宴?
- 我們知道搂橙,容器中的view都能夠用
.alignmentGuides()
modifier來顯式的返回對齊值,.alignmentGuides()
的第一個參數(shù)如果與Container Alignment不一樣笛坦,容器在布局的時候就會忽略這個view的.alignmentGuides()
- 它提供了容器中view的隱式alignment guide
大家看這段代碼:
struct Example3: View {
@State private var alignment: HorizontalAlignment = .leading
var body: some View {
VStack {
Spacer()
VStack(alignment: alignment) {
LabelView(title: "Athos", color: .green)
.alignmentGuide(.leading, computeValue: { _ in 30 } )
.alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 30 } )
.alignmentGuide(.trailing, computeValue: { _ in 90 } )
LabelView(title: "Porthos", color: .red)
.alignmentGuide(.leading, computeValue: { _ in 90 } )
.alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 30 } )
.alignmentGuide(.trailing, computeValue: { _ in 30 } )
LabelView(title: "Aramis", color: .blue) // use implicit guide
}
Spacer()
HStack {
Button("leading") { withAnimation(.easeInOut(duration: 2)) { self.alignment = .leading }}
Button("center") { withAnimation(.easeInOut(duration: 2)) { self.alignment = .center }}
Button("trailing") { withAnimation(.easeInOut(duration: 2)) { self.alignment = .trailing }}
}
}
}
}
它的運行效果如下圖所示:
可以很明顯的看出當我們切換container alignment的參數(shù)時区转,它內(nèi)部的view的alignment那些被忽略,那些被使用弯屈。
Frame Alignment
我一直在不斷的強調(diào)蜗帜,我們上邊看到的對齊方式,都應該把容器中的所有view看作一個整體资厉,alignment描述的是view之間的一種位置關系厅缺。
大家思考一下押框,即使.alignmentGuide
中的computeValue
返回值為0鳖粟,也不能說明該view保持不動困食。
如果我們把容器內(nèi)部的view看成一組厢破,那么Frame Alignment就非常容易理解了:
上邊3張圖片分別展示了VStack(alignment: .leading)
,VStack(alignment: .center)
和VStack(alignment: .trailing)
的情況姑躲,可以看出棉钧,他們內(nèi)部圖形的布局發(fā)生了變化恨统,但是他們3個整體都是居中對齊的怪蔑。
原因就是我們上一小節(jié)講的娩践,container alignment只影響容器內(nèi)的布局活翩,要讓容器內(nèi)的views整體左對齊或者居中,需要使用Frame Alignment.
.frame(maxWidth: .infinity, alignment: .leading)
關于Frame Alignment有一點需要特別注意翻伺,有時候看上去我們的設置沒有生效材泄,只要原因就是,在SwiftUI中吨岭,大多數(shù)情況下View的布局政策基于收緊策略拉宗,也就是View的寬度只是自己需要的寬度,這種情況下設置frame對齊當然就沒有意義了。
Multiline Text Alignment()
多行文本對齊就比較簡單了旦事,大家直接看圖就行了魁巩。
Interacting with the Alignment Guides
如果大家對上邊講的這些對齊方式還有疑惑,可以下載這里的代碼https://gist.github.com/swiftui-lab/793ca53ad1f2f0d7eb07aa23b54d9cbf,自己動手做一些交互姐浮,就應該能夠明白這些原理和用法了谷遂,放一張界面的截圖:
Custom Alignments
大多數(shù)情況,我們是不要自定義對齊的单料,使用系統(tǒng)提供的.leading
埋凯,.center
等等幾乎可以實現(xiàn)所有的UI效果,在本小節(jié)中扫尖,大家應該重點關注第二個例子白对,基本上只有這種情況,我們優(yōu)先考慮自定義對齊换怖。
自定義對齊的基本寫法如下:
extension HorizontalAlignment {
private enum WeirdAlignment: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d.height
}
}
static let weirdAlignment = HorizontalAlignment(WeirdAlignment.self)
}
- 決定是horizontal還是vertical
- 提供一個隱式對齊的默認值
我們小試牛刀甩恼,在上邊代碼中,我們自定義一個alignment沉颂,默認值返回view的高度条摸,這樣產(chǎn)生的效果如下:
可以看出,每個view的偏移都是它自身的高度铸屉,這樣的效果看上去還挺有意思钉蒲。完整代碼如下:
struct Example4: View {
var body: some View {
VStack(alignment: .weirdAlignment, spacing: 10) {
Rectangle()
.fill(Color.primary)
.frame(width: 1)
.alignmentGuide(.weirdAlignment, computeValue: { d in 0 })
ColorLabel(label: "Monday", color: .red, height: 50)
ColorLabel(label: "Tuesday", color: .orange, height: 70)
ColorLabel(label: "Wednesday", color: .yellow, height: 90)
ColorLabel(label: "Thursday", color: .green, height: 40)
ColorLabel(label: "Friday", color: .blue, height: 70)
ColorLabel(label: "Saturday", color: .purple, height: 40)
ColorLabel(label: "Sunday", color: .pink, height: 40)
Rectangle()
.fill(Color.primary)
.frame(width: 1)
.alignmentGuide(.weirdAlignment, computeValue: { d in 0 })
}
}
}
struct ColorLabel: View {
let label: String
let color: Color
let height: CGFloat
var body: some View {
Text(label).font(.title).foregroundColor(.primary).frame(height: height).padding(.horizontal, 20)
.background(RoundedRectangle(cornerRadius: 8).fill(color))
}
}
重點來了,我要說彻坛,使用自定義對齊最大的優(yōu)勢是:當在不同的view繼承分支上使用自定義對齊顷啼,會產(chǎn)生理想的效果。
大家看上圖昌屉,一個HStack包裹這一個Image和VStack钙蒙,VStack中有一組Text,當點擊某個Text的時候间驮,Image可以和點擊的Text對齊躬厌。
這就是我們上邊說的,對于屬于不同層次的view進行對齊竞帽,SwiftUI非常智能的知道我們想要這樣的效果扛施。完整代碼如下:
extension VerticalAlignment {
private enum MyAlignment : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.bottom]
}
}
static let myAlignment = VerticalAlignment(MyAlignment.self)
}
struct CustomView: View {
@State private var selectedIdx = 1
let days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
var body: some View {
HStack(alignment: .myAlignment) {
Image(systemName: "arrow.right.circle.fill")
.alignmentGuide(.myAlignment, computeValue: { d in d[VerticalAlignment.center] })
.foregroundColor(.green)
VStack(alignment: .leading) {
ForEach(days.indices, id: \.self) { idx in
Group {
if idx == self.selectedIdx {
Text(self.days[idx])
.transition(AnyTransition.identity)
.alignmentGuide(.myAlignment, computeValue: { d in d[VerticalAlignment.center] })
} else {
Text(self.days[idx])
.transition(AnyTransition.identity)
.onTapGesture {
withAnimation {
self.selectedIdx = idx
}
}
}
}
}
}
}
.padding(20)
.font(.largeTitle)
}
}
如果要自定義ZStack
的alignment,稍微麻煩一點屹篓,但原理都是一樣的相信大家都能夠理解疙渣,就直接上代碼了:
extension VerticalAlignment {
private enum MyVerticalAlignment : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.bottom]
}
}
static let myVerticalAlignment = VerticalAlignment(MyVerticalAlignment.self)
}
extension HorizontalAlignment {
private enum MyHorizontalAlignment : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let myHorizontalAlignment = HorizontalAlignment(MyHorizontalAlignment.self)
}
extension Alignment {
static let myAlignment = Alignment(horizontal: .myHorizontalAlignment, vertical: .myVerticalAlignment)
}
struct CustomView: View {
var body: some View {
ZStack(alignment: .myAlignment) {
...
}
}
}
總結
通過這篇文章,大家應該對Alignment Guide有了一個全面的了解抱虐,它應該成為我們?nèi)粘i_發(fā)進行布局的強大工具,現(xiàn)在回過頭來再看看這張圖片饥脑,是不是有那么一點感覺了呢恳邀?
注:上邊的內(nèi)容參考了網(wǎng)站https://swiftui-lab.com/alignment-guides/懦冰,如有侵權,立即刪除谣沸。