SwiftUI之AlignmentGuides
本质上,Alignment Guides属于SwiftUI中布局的知识点,在某些特殊场景下,使用Alignment Guides能起到事半功倍的效果,比如我们平时经常用的下边的这样的效果:

上图显示了,当切换背景容器的最大宽度时,使用Alignment Guides能够自动执行动画效果,这正是我们想要的,核心代码如下:
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
。
换句话说,我们对容器内的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之间的位置关系,系统在布局的时候,会把他们看成一个整体,然后在使用frame alignment guide对整体进行布局。
同样的道理,下边图片展示的是垂直对齐,我们就不再多做解释了:

通过上边这两个例子,我们得出一个结论:VStack需要水平对齐,HStack需要垂直对齐,虽然这听上去有点怪,但只需在头脑中想一想他们中view的排列方式,就不难理解。至于ZStack,即需要水平对齐,也需要垂直对齐,这个我们在下边的小节中,详细解释。
Alignment Guide中的疑惑
相信大家在代码中的很多地方会用到.leading
,在SwiftUI中,用到对齐的地方一共有下边几种:

这张图片覆盖了对齐所有的使用方式,现在大家可能是一脸茫然,但读完剩下的文章后,再回过头来看这张图片,就会发现,这张图片实在是太经典了,毫不夸张的说,你以后在SwiftUI中使用alignment guide的时候,头脑中一定会浮现出这张图片。
我们对上边的几个概念做个简单的介绍:
-
Container Alignment: 容器的对齐方式主要有2个目的,首先它定义了其内部views的隐式对齐方式,没有使用
alignmentGuides()
modifier的view都使用隐式对齐,然后定义了内部views中使用了alignmentGuides()
的view,只有参数与容器对齐参数相同,容器才会根据返回值计算布局 - Alignment Guide:如果该值和Container Alignment的参数不匹配,则不会生效
- Implicit Alignment Value:通常来说,隐式对齐采用的值都是默认的值,系统通常会使用和对齐参数相匹配的值
- Explicit Alignment Value:显式对齐跟隐式对齐相反,是我们自己用程序明确给出的返回值
- Frame Alignment:表示容器中views的对齐方式,把views看作一个整体,整体偏左,居中,或居右
- Text Alignment:控制多行文本的对齐方式
隐式和显式对齐的区别
每个view都有一个alignment,记住这一点非常重要,当我们使用.alignmentGuide()
设置对齐方式时,我们称之为显式,相反则称之为隐式。隐式的情况下,.alignmentGuide()
的返回值和它父类容器的对齐参数有关。
如果我们没有为VStack
, HStack
和 ZStack
提供alignment参数,默认值为center。
ViewDimensions
func alignmentGuide(_ g: HorizontalAlignment, computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View
func alignmentGuide(_ g: VerticalAlignment, computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View
我们已经知道了computeValue函数的返回值是一个CGFloat
类型,但我们不太清楚ViewDimensions
是个什么东西?很简单,我们可以直接查看它的系统定义:
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 }
}
很容易发现,通过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 })
,因此,它出现在hello的上边没什么问题,那么我如果把.alignmentGuide(HorizontalAlignment.leading, computeValue: { d in return 0 })
改成.alignmentGuide(HorizontalAlignment.leading, computeValue: { d in return d[.top] })
呢?
在设置leading对齐的时候使用了top对齐的数据,运行效果:

可以看出,完全符合我们的预期,world又向左偏移了100的距离,这就是我们上边说的用法,不过,通常情况下我们基本不需要这样操作。
类似d[HorizontalAlignment.leading]
这样的参数,我们都可简写成d[.leading]
,Swift能够非常智能的识别这些类型,但是center
除外,原因是HorizontalAlignment
和VerticalAlignment
都有center。
对齐类型
对于HorizontalAlignment
来说,有下边几个参数:
extension HorizontalAlignment {
public static let leading: HorizontalAlignment
public static let center: HorizontalAlignment
public static let trailing: HorizontalAlignment
}
当我们使用下标访问数据的时候,有两种方式:
d[.trailing]
d[explicit: .trailing]
d[.trailing]
表示获取d的隐式leading,也就是默认值,通常情况下,.leading
的值为0,.center
的值为width的一半,.trailing
的值为width。
d[explicit: .trailing]
表示获取d的显式的trailing,当没有通过.alignmentGuide()
指定值的时候,它返回nil,就像上一小节讲的一样,在ZStack
中,我们可以获取显式的对齐值
对于VerticalAlignment
来说,基本用法跟HorizontalAlignment
差不多,但它多了几个参数:
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)
}

重点来了,对齐描述的是容器内view之间的布局关系,由于computeValue函数的返回值都是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)) { ... }
本质上,它把horizontal和vertical封装在了一起,我们平时经常用的是下边这个写法,只是写法不同而已:
ZStack(alignment: .topLeading) { ... }
Container Alignment
所谓的容器的对齐方式指的是下边这里:
VStack(alignment: .leading)
HStack(alignment: .top)
ZStack(alignment: .topLeading)
那么它主要有什么作用呢?
- 我们知道,容器中的view都能够用
.alignmentGuides()
modifier来显式的返回对齐值,.alignmentGuides()
的第一个参数如果与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的参数时,它内部的view的alignment那些被忽略,那些被使用。
Frame Alignment
我一直在不断的强调,我们上边看到的对齐方式,都应该把容器中的所有view看作一个整体,alignment描述的是view之间的一种位置关系。
大家思考一下,即使.alignmentGuide
中的computeValue
返回值为0,也不能说明该view保持不动。
如果我们把容器内部的view看成一组,那么Frame Alignment就非常容易理解了:



上边3张图片分别展示了VStack(alignment: .leading)
,VStack(alignment: .center)
和VStack(alignment: .trailing)
的情况,可以看出,他们内部图形的布局发生了变化,但是他们3个整体都是居中对齐的。
原因就是我们上一小节讲的,container alignment只影响容器内的布局,要让容器内的views整体左对齐或者居中,需要使用Frame Alignment.
.frame(maxWidth: .infinity, alignment: .leading)

关于Frame Alignment有一点需要特别注意,有时候看上去我们的设置没有生效,只要原因就是,在SwiftUI中,大多数情况下View的布局政策基于收紧策略,也就是View的宽度只是自己需要的宽度,这种情况下设置frame对齐当然就没有意义了。
Multiline Text Alignment()
多行文本对齐就比较简单了,大家直接看图就行了。

Interacting with the Alignment Guides
如果大家对上边讲的这些对齐方式还有疑惑,可以下载这里的代码https://gist.github.com/swiftui-lab/793ca53ad1f2f0d7eb07aa23b54d9cbf,自己动手做一些交互,就应该能够明白这些原理和用法了,放一张界面的截图:

Custom Alignments
大多数情况,我们是不要自定义对齐的,使用系统提供的.leading
,.center
等等几乎可以实现所有的UI效果,在本小节中,大家应该重点关注第二个例子,基本上只有这种情况,我们优先考虑自定义对齐。
自定义对齐的基本写法如下:
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的高度,这样产生的效果如下:

可以看出,每个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))
}
}
重点来了,我要说,使用自定义对齐最大的优势是:当在不同的view继承分支上使用自定义对齐,会产生理想的效果。

大家看上图,一个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有了一个全面的了解,它应该成为我们日常开发进行布局的强大工具,现在回过头来再看看这张图片,是不是有那么一点感觉了呢?

注:上边的内容参考了网站https://swiftui-lab.com/alignment-guides/,如有侵权,立即删除。