SwiftUI - 界面布局知识点

前言

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中同样可以控制一个视图在纵向的位置。如果给它设置宽度或者高度,那效果也会不一样。

Swift 复制代码
HStack{
          Text("测试")
          Spacer()
       }
       .padding()
       .background(Color.green)

注意:HStack的背景颜色设置是.background,而不是.backgroundColor,background是在底部新建一个View。而且它与padding的顺序上也是有讲究的,谁在前谁在后效果都是不一样的。不妨可以试试看。

先调用padding,再调用background,效果如下:

先调用background,再调用padding,效果如下:

Swift 复制代码
HStack{
          Text("测试")
          Spacer()
      }
      .background(Color.green)
      .padding()
3、Devider()

SwiftUI中的表示分割线的一条线,在容器内以交叉轴方向做延伸,在不设置长度的情况下会撑满容器的最大可显示区域交叉轴。这样容器类的区域也会随着Devider去放大。当然也可以给它设置相应的宽或高来满足我们的需求。

4、Group与GroupBox

字面意思看是一个"分组"和"分组盒子",可以将一组里的所有视图设置统一的样式,示例如下:

Swift 复制代码
Group{
            HStack{
                Text("测试1")
                Spacer()
            }
            HStack{
                Text("测试2")
                Spacer()
            }
        }.padding()
         .background(Color.green)

对比代码:

Swift 复制代码
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来实现。

Swift 复制代码
Button(action: {
        }){
            Text("")
        }.frame(width: 50, height: 50).background(Color.green).cornerRadius(25)
            .overlay(){
                Image(systemName: "person")
            }

就很简单的实现了按钮上添加图片的功能。默认图片展示在按钮中心点上。

利用ZStack实现相同功能如下:

Swift 复制代码
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)的位置。

Swift 复制代码
//第一段
        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%,而不管其内容如何:

Swift 复制代码
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然后在其下方放置更多文本,你可以看到它的实际效果,如下所示:

Swift 复制代码
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视图,以便它可以全局、局部地打印出框架,并使用自定义坐标空间。

Swift 复制代码
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()。

相关推荐
ii_best12 小时前
ios按键精灵自动化的脚本教程:自动点赞功能的实现
运维·ios·自动化
app开发工程师V帅1 天前
iOS 苹果开发者账号: 查看和添加设备UUID 及设备数量
ios
CodeCreator18181 天前
iOS AccentColor 和 Color Set
ios
iOS民工1 天前
iOS keychain
ios
m0_748238921 天前
webgis入门实战案例——智慧校园
开发语言·ios·swift
Legendary_0081 天前
LDR6020在iPad一体式键盘的创新应用
ios·计算机外设·ipad
/**书香门第*/2 天前
Laya ios接入goole广告,搭建环境 1
ios
wakangda2 天前
React Native 集成 iOS 原生功能
react native·ios·cocoa
Swift社区2 天前
Excel 列名称转换问题 Swift 解答
开发语言·excel·swift
crasowas3 天前
iOS - 超好用的隐私清单修复脚本(持续更新)
ios·app store