前言
SwiftUI采用的布局方式是和Flutter一样是弹性布局
,而不是iOS之前的坐标轴
的方式布局,不用准确的设置出位置大小,只需要设置当前视图大小及视图间排布的方式。灵活性增强,布局操作简便,SwiftUI与Flutter布局原理一样,学完一个再学另一个都很方便。
1、VStack 、HStack 、ZStack、LazyVStack、LazyHStack
SwiftUI里的三大容器视图,在布局中是基本的容器类
视图,子视图(如Text、Image、Button等)放在它们里面按一定的规则排序。子视图之间默认的spacing=8
(即不设置时候间距为8),子视图默认的padding=16
(需要设置.padding()才会有),如Text,默认的.padding()=.padding(16)。容器视图的区域为所有子视图所占的矩形空间,如果只有一个子视图,那大小就是子视图的大小。VStack
:纵向布局容器,容器内子视图呈纵向排列,从上往下排列。HStack
:横向布局容器,容器内子视图呈横向排列,从左往右排列。ZStack
:深度布局容器,容器内子视图呈前后排列,从里到外排列(屏幕为参照),默认的优先级zIndex=0。
LazyVStack
:纵向布局容器,容器内子视图呈纵向排列,从上往下排列,LazyVStack特点是仅在需要时创建,如果容器子视图太多,超时屏幕太多,可以使用它节省内存。LazyHStack
:横向布局容器,容器内子视图呈横向排列,从左往右排列,LazyHStack特点是仅在需要时创建,如果容器子视图太多,超时屏幕太多,可以使用它节省内存,只加载屏幕上需要展示的View,当滑动时才去展示更多的View,即触发了懒加载机制,当我们去掉ScrollView后,发现无法触发懒加载。
当我们把ForEach替换成用Group包装的多个组后,也不能实现懒加载效果。
所以LazyStack想要触发懒加载机制,ScrollView及ForEach缺一不可。
2、Spacer
Spacer()
:一个看似透明的视图,在布局中起重要作用,它起一个撑满的作用,比如Hstack中的一个Text想在屏幕左边,那么右边添加一个Spacer即可,Spacer就会将右边剩余部分撑满,Text就会被撑到左边。在Vstack中同样可以控制一个视图在纵向的位置。如果给它设置宽度或者高度,那效果也会不一样。
HStack{
Text("测试")
Spacer()
}
.padding()
.background(Color.green)
注意:HStack
的背景颜色设置是.background,而不是.backgroundColor,background是在底部新建一个View。而且它与padding的顺序上也是有讲究的,谁在前谁在后效果都是不一样的。不妨可以试试看。
先调用padding,再调用background,效果如下:
先调用background,再调用padding,效果如下:
HStack{
Text("测试")
Spacer()
}
.background(Color.green)
.padding()
3、Devider()
SwiftUI中的表示分割线的一条线,在容器内以交叉轴方向做延伸,在不设置长度的情况下会撑满容器的最大可显示区域交叉轴。这样容器类的区域也会随着Devider去放大。当然也可以给它设置相应的宽或高来满足我们的需求。
4、Group与GroupBox
字面意思看是一个“分组”和“分组盒子”
,可以将一组里的所有视图设置统一的样式,示例如下:
Group{
HStack{
Text("测试1")
Spacer()
}
HStack{
Text("测试2")
Spacer()
}
}.padding()
.background(Color.green)
对比代码:
VStack{
Group{
Text("测试组一")
}
if #available(iOS 14.0, *) {
GroupBox{
Text("测试组二")
}.padding().background(Color.red)
GroupBox{
Text("测试组三")
}.padding().colorMultiply(.red)
} else {
// Fallback on earlier versions
}
ForEach(0...3,id:\.self){
index in
if #available(iOS 14.0, *) {
GroupBox(label: Text("第\(index+1)组"), content: {
Text("Content").frame(width: 120, height: 20, alignment: .center).background(Color.green)
})
} else {
// Fallback on earlier versions
}
}
}
对比效果:
可见GroupBox就是一个分组的盒子,而且可以嵌套使用,图中外Box显示全黄色以及内Box显示全红色的效果使用的是colorMultiply
而不是background,因为background只是在底部添加一个View,colorMultiply则是在最顶部也就是屏幕最外面添加一个遮罩层,就像在做颜色混合计算一样,覆盖上去,会影响子视图显示的颜色(如果子视图设置了别的颜色,此处未设置,所以随Box.colorMultiply颜色)。
OutlineGroup
:类似文件夹的分层效果,可实现树状结构的分层效果,可折叠,可展开。
DisClosureGroup
: 可折叠的分组,类似于List里的.listStyle(.sidebar)
样式,是GroupBox中的子视图可折叠可展开样式。嵌套使用时候即可实现OutlineGroup分层结构效果,树状结构效果上个人感觉比OutlineGroup效果更好。
ControlGroup
:类似于UIKit中的Segmented
的样式。如果想改变样式,可以更改.controlgRgoupStyle()
。
5、overlay
在实现前后顺序的功能,布局上除了ZStack,我们还可以使用overlay
。
如系统计算器里按钮上的文字就可以使用overlay来实现。
Button(action: {
}){
Text("")
}.frame(width: 50, height: 50).background(Color.green).cornerRadius(25)
.overlay(){
Image(systemName: "person")
}
就很简单的实现了按钮上添加图片的功能。默认图片展示在按钮中心点上。
利用ZStack实现相同功能如下:
ZStack{
Button(action: {
}){
Text("")
}.frame(width: 50, height: 50).background(.red).cornerRadius(25)
Image(systemName: "person")
}
而且区别就是.overlay是按钮的一个Modifier,而ZStack是一个容器。具体的还是要根据项目实际功能来选择哪种方式。
6、绝对位置、相对位置
position(x:,y:):绝对位置,设置视图的中心点在距离左上角(x,y)的位置。
//第一段
Text("测试")
.font(.title)
.background(.red)
.padding()
.position(x:100,y:100)
//第二段
Text("测试")
.padding()
.font(.title)
.background(.red)
.position(x:100,y:100)
//第三段
Text("测试")
.padding()
.font(.title)
.position(x:100,y:100)
.background(.red)
//第四段
Text("测试")
.padding()
.font(.title)
.background(.red)
.position(x:100,y:100)
.background(.green)
效果如下:
可以看出增加position后显示区域感觉变大了,也就是安全区域,原因是position会新建一个View作为Text的父视图,所以position之后的颜色设置的是position所返回的View的背景颜色。
当我们使用position()时,我们得到一个占据所有可用空间的新视图,因此它可以将其子项(文本)定位在正确的位置。
offset():相对位置,相对position来说不会新建一个父视图,而是直接将中心点按offset所标大小移动。只是去改变显示的位置。
当我们使用offset()修饰符时,我们是在更改应呈现视图的位置,而不会实际更改其底层几何图形 。注意:
从这里我们可以看出函数响应式代码的细节,要注意顺序的前后对结果的影响。
7、GeometryReader
使用它的大小和坐标来确定子视图的布局,
在使用 GeometryReader时,你应该始终牢记 SwiftUI 的三步布局系统:父级为子级建议尺寸,子级使用它来确定自己的尺寸,父级使用它来适当地定位子级。
在其最基本的用法中,GeometryReader的作用是让我们读取父级建议的尺寸,然后使用它来操纵我们的视图。例如,我们可以用GeometryReader使文本视图拥有所有可用宽度的 90%,而不管其内容如何:
struct ContentView: View {
var body: some View {
GeometryReader { geo in
Text("Hello, World!")
.frame(width: geo.size.width * 0.9)
.background(.red)
}
}
}
geo传入的参数是GeometryProxy,它包含建议的大小、已应用的任何安全区域插图,以及我们稍后将查看的读取帧值的方法。
GeometryReader有一个有趣的副作用,一开始可能会让你大吃一惊:返回的视图具有灵活的首选大小,这意味着它将根据需要扩展以占用更多空间。如果将GeometryReader放入一个VStack然后在其下方放置更多文本,你可以看到它的实际效果,如下所示:
struct ContentView: View {
var body: some View {
VStack {
GeometryReader { geo in
Text("Hello, World!")
.frame(width: geo.size.width * 0.9, height: 40)
.background(.red)
}
Text("More text")
.background(.blue)
}
}
}
你会看到“更多文本”被推到屏幕底部,因为GeometryReader占用了所有剩余空间。要查看它的实际效果,请将background(.green)其添加为GeometryReader的修饰符,然后你就会看到它有多大。注意:这是首选大小,而不是绝对大小,这意味着它仍然可以根据其父级灵活调整。
当谈到读取视图的框架时,GeometryProxy提供了一种frame(in:)方法而不是简单的属性。这是因为“框架”的概念包括 X 和 Y 坐标,它们孤立起来没有任何意义——你想要视图的绝对 X 和 Y 坐标,还是它们的 X 和 Y 坐标与其父坐标的比较?
SwiftUI 将这些选项称为coordinate spaces(坐标空间),特别是这两个称为全局空间(测量我们的视图相对于整个屏幕的框架)和局部空间(测量我们的视图相对于其父级的框架)。我们还可以通过将coordinateSpace()修饰符附加到视图来创建自定义坐标空间——然后它的任何子元素都可以读取相对于该坐标空间的框架。
为了演示坐标空间是如何工作的,我们可以在各种堆栈中创建一些示例视图,将自定义坐标空间附加到最外面的视图,然后将一个添加到其中的一个onTapGesture视图,以便它可以全局、局部地打印出框架,并使用自定义坐标空间。
struct OuterView: View {
var body: some View {
VStack {
Text("Top")
InnerView()
.background(.green)
Text("Bottom")
}
}
}
struct InnerView: View {
var body: some View {
HStack {
Text("Left")
GeometryReader { geo in
Text("Center")
.background(.blue)
.onTapGesture {
print("Global center: \(geo.frame(in: .global).midX) x \(geo.frame(in: .global).midY)")
print("Custom center: \(geo.frame(in: .named("Custom")).midX) x \(geo.frame(in: .named("Custom")).midY)")
print("Local center: \(geo.frame(in: .local).midX) x \(geo.frame(in: .local).midY)")
}
}
.background(.orange)
Text("Right")
}
}
}
struct ContentView: View {
var body: some View {
OuterView()
.background(.red)
.coordinateSpace(name: "Custom")
}
}
该代码运行时获得的输出取决于你使用的设备,但这是我得到的:
全局中心:189.83 x 430.60
定制中心:189.83 x 383.60
局部中心:152.17 x 350.96
这些尺寸大多不同,因此希望你能全面了解这些框架的工作原理:
全局中心 X 为 189 意味着几何阅读器的中心距屏幕左边缘 189 点。
全局中心 Y 为 430 表示文本视图的中心距屏幕顶部边缘 430 点。这并没有死在屏幕中央,因为顶部比底部有更多的安全区域。
自定义中心 X 为 189 意味着文本视图的中心距离拥有“自定义”坐标空间的任何视图的左边缘 189 点,在我们的例子中,这是因为我们将OuterView附加到ContentView. 该数字与全局位置匹配,因为OuterView水平地从边到边延伸。
自定义中心 Y 为 383 表示文本视图的中心距 OuterView的上边缘 383 点。该值小于全局中心 Y,因为OuterView没有延伸到安全区域。
局部中心 X 为 152 意味着文本视图的中心距离其直接容器的左边缘 152 点,在本例中为GeometryReader.
350 的局部中心 Y 意味着文本视图的中心距离其直接容器的顶部边缘 350 点,这也是GeometryReader.
你要使用哪个坐标空间取决于你要回答的问题:
想知道这个视图在屏幕上的什么位置?使用全局空间.global。
想知道此视图相对于其父视图的位置吗?使用本地空间.local。
知道这个视图相对于其他视图的位置是什么?使用自定义空间.named()。