布局
SwiftUI 的布局算法⾮常直接:⽗视图向它的⼦视图提供⼀个建议尺⼨ (proposed size),⼦视图基于这个建议决定⾃⼰的⼤⼩,并将结果汇报给⽗视图。然后,⽗视图在⾃⼰的坐标系中放置这个⼦视图。
- SwiftUI 的布局算法是沿着视图树从上向下进⾏处理的
例如:
- VStack 是根视图,所以它会接收到整个屏幕的安全区域 (safearea) 作为建议尺⼨。
- VStack 在决定⾃⼰的尺⼨时,它会以递归的⽅式先向它的⼦视图提出尺⼨建议。
- Image 按照地球 (globe) 符号的尺⼨汇报⾃⼰的尺⼨,Text 则基于建议尺⼨和它需要渲染的字符串来报告⾃⼰的尺⼨ 。
- 然后,VStack 将两个⼦视图垂直排列在⼀起,并在两个视图之间插⼊默认的间距。
- VStack 在计算⾃⼰的⼤⼩时,会把⼦视图的框架 (frame) 合并,然后把尺⼨报告回去。
Text
实际开发过程中,复杂的视图中,可能出现这样一种情况: Text没有换行,而是显示了...
(不好模拟)。如果要防止这种情况,可以使用.fixedSize(horizontal: false, vertical: true)
。
来看下原理:
默认情况下,Text 视图会去适应任意的建议尺⼨,设法让⾃⼰适配 (不超过) 这个尺⼨。Text 使⽤多种策略,按照以下顺序来实现这⼀⽬标:将⽂本分成多⾏ (英⽂内容按单词换⾏),单词内换⾏ (使⽤连词符号),截断⽂本最后裁剪⽂本。Text 始终都会返回它所需要渲染的内容的⼤⼩,这个尺⼨在宽度上⼀定⼩于或等于建议的宽度,在⾼度上除⾮提议的是 0⨉0,否则⾄少是⼀⾏的⾼度。
如果我们将 .fixedSize()
应⽤于 Text,它将使⽤理想尺⼨,因为 fixedSize 向Text 所建议的尺⼨是 nil⨉nil。Text 的理想尺⼨是在不换⾏和不截断的情况下呈现内容所需的尺⼨。下⾯是 Text("Hello, World!").fixedSize()
在不同建议尺⼨下的渲染情况:
fixedSize(horizontal: false, vertical: true)
:
horizontal
参数设置为false
时,意味着该视图在水平方向上不会被强制固定为其固有内容的大小。它会根据父视图的布局约束,在水平方向上进行灵活的扩展或收缩。vertical
参数设置为true
时,该视图在垂直方向上会被强制固定为其固有内容的大小。这意味着该视图在垂直方向上不会根据父视图的布局约束而被拉伸或压缩。例如,如果该视图包含文本,那么文本的高度会根据文本内容的实际高度确定,而不会因为父视图的垂直布局约束而改变其垂直尺寸。
struct OneLineView: View {
var body: some View {
VStack {
Text("勃,三尺微命,一介书生。无路请缨,等终军之弱冠;有怀投笔,慕宗悫之长风。舍簪笏于百龄,奉晨昏于万里。非谢家之宝树,接孟氏之芳邻。他日趋庭,叨陪鲤对;今兹捧袂,喜托龙门。杨意不逢,抚凌云而自惜;钟期既遇,奏流水以何惭?")
.font(.title3)
.fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(.leading)
Image(systemName: "circle")
Spacer()
}
}
}
struct OneLineView: View {
var body: some View {
VStack {
Text("勃,三尺微命,一介书生。无路请缨,等终军之弱冠;有怀投笔,慕宗悫之长风。舍簪笏于百龄,奉晨昏于万里。非谢家之宝树,接孟氏之芳邻。他日趋庭,叨陪鲤对;今兹捧袂,喜托龙门。杨意不逢,抚凌云而自惜;钟期既遇,奏流水以何惭?")
.font(.title3)
.fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(.leading)
Image(systemName: "circle")
Spacer()
}
}
}
Shape
⼤部分内置的形状类型 (Rectangle,RoundedRectangle,Capsule,以及Ellipse) 接受从零到⽆限的任意⼤⼩的建议尺⼨,并且会填充所有可⽤空间。Circle 是⼀个特例:它会按照建议尺⼨的短边作为直径进⾏适配,然后将圆形的实际尺⼨进⾏汇报。如果我们使⽤ nil 来对形状进⾏尺⼨建议 (⽐如,我们把形状包装到 fixedSize ⾥),它将使⽤ 10⨉10 这个默认尺⼨。
Color
当直接将颜⾊当作视图使⽤时,⽐如 Color.red,从视图布局的视⻆来看,它的⾏为和 Rectangle().fill(…)
是⼀样的。
但是有⼀个特例:如果我们将⼀个颜⾊放在与⾮安全区域有接触的背景中,颜⾊将神奇地“渗⼊”到⾮安全区域中去。虽然这个⾏为并不会影响布局,但我们还是想在这⾥提⼀下,因为我们⼤概迟早都会遇到这个问题。如果我们想要避免这种情况,可以使⽤ .background 上的 ignoresSafeAreaEdges 参数,或者使⽤Rectangle().fill(…) 来取代 Color。
image
默认情况下,Image 视图会报告⼀个固定值:也就是它所持有的图⽚的尺⼨。⼀旦我们在 Image 上调⽤ .resizable(),这个视图就会完全灵活可变:Image 将会接受任意的建议尺⼨,并将它报告回去,同时将图像压缩或者拉伸到这个尺⼨。
在实践中,实际上任何⼀个 resizable 的图⽚都会和 .aspectRatio(contentMode:) 或 .scaleToFit() 修饰器⼀同使⽤,以避免图⽚产⽣变形。
Divider
Spacer
在⽔平堆栈和垂直堆栈之外,Spacer 接受从最⼩⻓度到⽆限⼤的所有建议尺⼨。然⽽,当 Spacer 放置在⽔平或垂直堆栈中时,其⾏为会发⽣变化:在垂直堆栈中,Spacer 接受从其最⼩⻓度到⽆限⼤的任何⾼度,但报告的宽度为零。在⽔平堆栈中,它的⾏为相同 (只是轴互换)。除⾮使⽤ Spacer 初始化⽅法中的minLength 参数指定⼀个⻓度,否则 Spacer 的最⼩⻓度会是默认填充 (padding)的⻓度,
例如如下实现了文本左对齐:
然而更好的方式肯能是这个(因为spacer 是有最⼩⻓度的 (等于默认的间
距)):
Text("文本左对齐")
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.secondary.opacity(0.1))
Text("文本左对齐")
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.secondary.opacity(0.1))
视图修饰器
padding
.padding
修饰器使⽤它收到的内边距值来修改建议尺⼨,它会从建议尺⼨的对应边上减去这个边距值。修改后的尺⼨将被提供给 .padding 的⼦视图 (也就是这个修饰器所作⽤的视图)。
固定框架 fixed frame
固定框架 (fixed frame) 修饰器 .frame(width:height:alignment) 所拥有的布局⾏为⾮常简单:它把所指定的尺⼨原封不动地提供给⼦视图,同时不管⼦视图所报告的尺⼨是多少,总是把这个指定的尺⼨作为⾃⼰的⼤⼩进⾏汇报。换句话说,固定框架是⼀个尺⼨完全固定的不可⻅的视图,我们可以⽤它来将指定的尺⼨提供给另⼀个视图。
如果我们只指定宽度或者⾼度,⽽把另⼀个参数设定为 nil 或者完全省略掉它,那么固定框架将不会对该维度产⽣影响。向固定框架提供的建议尺⼨在该维度上将被直接转发给⼦视图,固定框架在这个维度上也会将⼦视图所报告的尺⼨作为⾃⼰的尺⼨。
灵活框架 (flexible frame)
灵活框架 (flexible frame) 的 API ⽀持⾮常多的参数:我们不仅可以为框架的宽和⾼提供最⼩、最⼤和理想值,还可以为它指定对⻬⽅式。灵活框架的⾏为并不是⾮常直观,但是它们是很有⽤的⼯具,学习它们的⾏为⼗分重要。我们现在先忽略掉理想的宽度和⾼度参数,只考虑最⼤值和最⼩值这两个定义边界的参数。
灵活框架会把所获得的建议尺⼨,按照指定的最⼩值和最⼤值进⾏限制:如果传⼊的尺⼨没有落在最⼩和最⼤值之间,则强制使⽤最⼩和最⼤值作为建议尺⼨并提供给框架的⼦视图。
.frame(maxWidth: .in!nity)
模式确保了灵活框架的宽度⾄少和被建议的宽度相同,如果⼦视图的宽度要⽐框架接受的建议宽度还宽的话,则使⽤⼦视图的宽度。通常,这个模式被⽤来创建占据整个可⽤宽度的视图。
例如,通过frame(minWidth: 0, maxWidth: .infinity)
可以实现一个简单的等分布局(总使用建议的宽度):
.aspectRatio
aspectRatio 修饰器将会计算出⼀个能够适配进建议尺⼨的宽⾼⽐为 4/3 的矩形,然后将它作为建议尺⼨提供给⼦视图。对于⽗视图,它总是把⼦视图的尺⼨汇报上去,⽽不去理会建议尺⼨或者设定的⽐例。
最常见的是配合图片使用:
overlay & background
略。
容器
vstack hstack
下图描述了宽度改变时,文字长度的变化。为什么被换行了呢?分析原因:
分配算法:
- ⾸先,HStack 会确定它的⼦视图们的灵活性。两个颜⾊视图是⽆限灵活的,不管向它们提供什么样的尺⼨,它们都会欣然接受。但是 Text 的宽度会有上限,Text 的宽度可能介于 0 到它的理想宽度之间,但是绝对不可能超过理想宽度。
- HStack 根据⼦视图的灵活性从低到⾼进⾏排序。它会跟踪所有的剩余⼦视图和可⽤的剩余宽度。
- 只要还有剩余的⼦视图,HStack 就会把剩余的宽度除以⼦视图的数量,然后把结果作为建议宽度提供给这个⼦视图。
为了简单起⻅,我们假设⽂本的理想宽度是 100。当我们向 HStack 提供180⨉180 时,因为剩余三个⼦视图,它⾸先把宽度除以 3,然后把 180/3,也就是60,提供给灵活性最低的视图,也就是 Text。接下来 Text 根据需要进⾏换⾏或者裁断。我们假设⽂本的尺⼨结果为 50⨉40 (插⼊了⼀个换⾏)。于是两个矩形颜⾊分别会得到 130/2 和 65/1 的宽度。由于这个算法,就算⽂本原本其实有⾜够的空间显示在⼀⾏,它也还是会进⾏换⾏。
ZStack
乍⼀看,ZStack 似乎做的事情和 overlay 或者 background 是⼀样的,但是实际上它们的⾏为不尽相同。如上所述,overlay 和 background 使⽤主要⼦视图的尺⼨,并把次要⼦视图的尺⼨丢弃掉。当使⽤ ZStack 时,它的所有⼦视图的frame 将组合起来,进⾏并集 (union) 操作,并依此计算 ZStack ⾃身的尺⼨。
滚动视图
滚动视图本身,在滚动轴的⽅向上会接受⽗视图所提供的建议尺⼨;在另⼀个轴上,则使⽤它的内容的尺⼨。⽐如,如果我们把⼀个垂直的滚动视图作为根视图的话,它的⾼度就是会整个安全区域,宽度则取决于滚动内容。
在滚动⽅向上,滚动视图本质上具有⽆限的空间,它的内容在这个轴上也可以⽆限增⻓。因此,滚动视图在它的滚动轴上将使⽤ nil 作为建议尺⼨,在另外的轴上则将滚动视图⾃身所收到的尺⼨不加修改地提供给内容视图。
GeometryReader
GeometryReader 接受被建议的尺寸并将这个尺⼨通过⼀个GeometryProxy 报告给它的视图构建闭包。
为了排除对布局的影响,可以修改代码,放到 background 或者 overlay 修饰器中: