SwiftUI之AlignmentGuides
2020/6/29 23:27:18
本文主要是介绍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
如果大家对上边讲的这些对齐方式还有疑惑,可以下载这里的代码gist.github.com/swiftui-lab…,自己动手做一些交互,就应该能够明白这些原理和用法了,放一张界面的截图:
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/,如有侵权,立即删除。
这篇关于SwiftUI之AlignmentGuides的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值