什么是SwiftUI?
SwiftUI是2019年WWDC大会上,苹果发布的基于Swift语言构建的全新UI框架,开发者可通过它快速为所有的Apple平台创建美观、动态的应用程序;
SwiftUI的运行速度优于UIKit,他减少了界面的层次结构,因此可以减少绘制步骤,并且他完全绕过了CoreAnimation,直接进入Metal,可以有优秀的渲染性能;
SwiftUI 就是⼀种声明式的构建界面的用户接口工具包;
SwiftUI使用声明式的语法构建UI,我们只需要向系统声明UI的View样式,以及View如何转换状态,其他的过程都交给系统去处理;
声明式语法和指令式语法的区别:
声明式的我们需要提前声明好每个view的各种状态,以及状态转变的条件。后续界面和用户在互动时,系统会帮我们自动进行状态切换;
指令式的我们需要给每个view先设置好默认状态,后续界面和用户在互动时,需要通过指令不停的去转变view的状态;
因此声明式的UI是提前声明好各种状态,系统会自动帮我们进行状态切换。指令式的UI是通过我们设定的指令来转换状态;
比如界面调整、用户交互、机型适配,UIKit都需要手动调整view,对于SwiftUI我们只需要声明好我们想要的样式,系统会帮我们去调整view;
总结起来,SwiftUI比UIKit更加抽象化;
1)SwiftUI的优点
1.1)统一苹果终端
在 SwiftUI 出现之前,苹果不同的设备之前的开发框架并不互通,增加开发者所需消耗的时间精力,也不利于构建跨平台的软件体验;
SwiftUI具有了跨平台性,苹果的平台都可以使用,iOS、macOS、tvOS、watchOS;
1.2)降低界面开发难度
UIKit 的基本思想要求ViewController 承担绝⼤部分职责,它需要协调 model,view 以及⽤⼾交互。这带来了巨⼤的sideeffect 以及⼤量的状态;
SwiftUI是声明式的构建方式,我们只需要声明好界面系统会自动转换状态,搭建界面更加的简单;
1.3)更加高效
默认使用Metal渲染,性能非常高,比UIKit要好;
更扁平化的内联数据结构去分配内存,值类型。占用内存很少(所以在轻应用的开发更适合使用SwiftUI);
代码量相比UIKit要更少,效率更高;
1.4)更好的配合Swift语言
SwiftUI 使用了大量 Swift 的语言特性;
2)SwiftUI的特性
2.1)声明式语法
与UIKit布局相比,更加的抽象化,只需要向系统声明界面样式以及样式变化条件,其他的系统会帮我们实现,不需要我们自己去调整视图;
2.2)界面元素的组件化
UIKit耦合了很多的操作逻辑,很难进行移植,更遑论组件化了;
而SwiftUI仅仅声明界面样式,所以是可以将复杂视图的拆分出来组件化;
甚至还可以在其他平台使用,以此跨平台;
一般我个人会将视图组件区分为基础组件、布局组件和功能组件;
2.3)与UIKit互相兼容
把 UIKit 中已有的部分进行封装,提供给 SwiftUI 使用。开发者需要做的仅仅是遵循UIViewRepresentable协议即可;
并且在已有的项目中,也可以仅用 SwiftUI 制作一部分的 UI 界面;
两种代码的风格是截然不同的,但在使用上却基本没有性能的损失。在最终的运行效果上,用户也无法分辨出两种界面框架的不同;
2.4)真实数据源(Source of truth)(重点特性)
SwiftUI中的数据源一定会是真实的,也就是准确的;
在UIKit中,一个view的状态由多种因素导致的,不同的来源,不同的逻辑操作(因此需要考虑及时更新界面);
在SwiftUI中,只要在属性声明时加上@State关键词,就可以将该属性和界面元素联系起来,在每次数据改动后,都有机会决定是否更新视图,系统将所有的属性都集中到一起进行管理和计算,也不再需要手写刷新的逻辑;
2.5)设计工具和快速预览功能
Xcode 包含直观的设计工具,只需拖放操作就能使用 SwiftUI 轻松构建界面,同时支持实时预览页面的变化;
SwiftUI中常用的View和Modifiers
SwiftUI通过View视图搭建界面,使用Modifiers修饰器来修饰视图。系统提供了大量的视图和修饰器,并且还可以让我们自定义修饰器。
既可以手动写,也可以直接拖出到代码区或者预览区。这三种方式的结果都是一样的。
1)Text
显示一行或多行的只读文本视图,类似于UIKit中的UIlabel;
Text("我是一个Text").foregroundColor(.red)
2)Label
显示一个标签组件,支持图片与标题的展示;
Label("Rain", systemImage: "cloud.rain")
3)Button
显示一个按钮组件,类似于UIKit中的UIButton;
Button {
print("button点击响应")
} label: {
Text("我是按钮")
}
4)Link
通过提供目标URL和标题来创建链接;
Link(destination: URL(string:"https://www.baidu.com/")!) {
Text("Link")
}
5)Image
显示一个图片组件;
Image("image name")
.resizable()
.aspectRatio(contentMode: .fit)
也可以通过AsyncImage实现异步加载网络图片的组件;
AsyncImage(url: URL(string: "image url")) { image in
image.resizable()
} placeholder: {
Image("placeHolder image")
}
6)TextEditor
显示可编辑文本界面的控件。相当于UITextView;
TextEditor(text:
.constant("Placeholder"))
.frame(width: 100, height: 30, alignment: .center)
7)TextField
显示文本输入框。相当于UITextView;
TextField("首字母默认大写", text: $str).frame(width: 100, height: 56, alignment: .center)
.textInputAutocapitalization(.never)
.onSubmit {
print("我点击了!")
}
textInputAutocapitalization为设置自动大小写的属性;
8)NavigationView
用于做页面间的导航跳转;
var body: some View {
NavigationView{
List{
VStack {
...
}
}.navigationBarTitle("Todo Items")
}
}
- 使用navigationBarTitle方法给控件设置导航栏的标题;
- 注意navigationBarTitle修饰符属于列表视图,而不是导航视图;
- 这是因为导航视图从右边通过push来显示新界面;
- 每个界面都有自己的标题。如果标题是附加到导航视图,标题就被固定了;
- 通过附加的标题到导航视图的里面内容,标题可以更改为其内容的变化;
NavigationView {
VStack {
...
NavigationLink(destination:
VStack {
Text(todo.name)
Image(todo.category).resizable().frame(width: 200, height: 200)
}
){
HStack {
...
}
}
}.navigationBarTitle("Nav Title")
}
- 注意,我们必须向NavigationLink提供一个参数destination,也就是点击项目时显示的视图;
- 这里代码中可以看到,视图将包括:Text和Image;
- 当运行应用程序,点击一个item就会跳转到另一个界面,界面显示选择的项目的详细信息;
- 新界面的顶部栏也会显示带有上一个项目的符号;
SwiftUI中的布局
UI通常由多种不同类型的视图组合而成。我们如何对他们进行分组以及布局定位?此时就需要使用stacks。我们可以使用三种堆栈来对UI进行分组:
- HStack - 水平排列其子视图;
- VStack - 垂直排列其子视图;
- ZStack -根据深度排列子视图(例如从后到前);
在这三种Stack的基础上还有一种懒加载的Stack,叫lazyStack;
除此之外还需要了解绝地位置和相对位置的使用;
注意: SwiftUI没有坐标系这种说法,使用弹性布局。类似于HTML的布局方式;
SwiftUI中List的使用
1)List的创建
var body: some View {
List{
HStack{
Image("work").resizable().frame(width: 50, height: 50)
Text("Write SwiftUI book")
}
HStack{
Image("personal").resizable().frame(width: 50, height: 50)
Text("Read Bible")
}
HStack{
Image("family").resizable().frame(width: 50, height: 50)
Text("Bring kids out to play")
}
HStack{
Image("family").resizable().frame(width: 50, height: 50)
Text("Fetch wife")
}
HStack{
Image("family").resizable().frame(width: 50, height: 50)
Text("Call mum")
}
}
}
- 通过List添加多行数据;
- 每一行包含一个图像和一个水平文本,通过HStack来包装;
- 因为图像大小不同,大的图像会被扩展,除了屏幕大小,只显示了一部分。为了解决这个问题,我们应用. resizable修改器使图像适合于使用面积;
- 然后应用.frame修饰符将图像的大小限制为一个自定义的框架;
2)List的动态性
可通过@State修饰数据源实现List列表的实时刷新;
3)ID标识
通过ForEach构建List元素时可以为每一个item设置id,一般可以通过数据源内对应该item的数据中的内容定义id,也可以直接使用数据本身self;
var body: some View {
List{
ForEach(datas, id:\.name){ data in
HStack{
Image(datas.category) .resizable().frame(width: 50, height: 50)
Text(datas.name)
}
}
}
}
SwiftUI中的属性包装器
1)@State
SwiftUI管理声明为state的存储属性。当值发生变化时,SwiftUI会更新视图层次结构中依赖于该值的部分。使用@State作为存储在视图层次结构中的给定值的唯一真值来源;
@State修饰的属性虽然是存储属性,但是我们可以进行读写操作;
父视图和子视图进行传递该属性只能是值传递;
需要在属性名称前加上一个美元符号$来获得这个值;
struct ContentView: View {
@State private var str: String = ""
var body: some View {
VStack {
TextField("Placeholder", text: $str)
Text("\(str)")
}
}
}
- 在str上设置了@State修饰,那么我在文本输入框中输入的数据,就会传入到str中;
- 同时str又绑定在文本视图上,所以会将文本输入框输入的文本显示到文本视图上;
- 这就是数据绑定的快捷实现;
2)@Binding
@State修饰的属性是值传递,因此在父视图和子视图之间传递属性时。子视图针对属性的修改无法传递到父视图上;
Binding修饰后会将属性会变为一个引用类型,视图之间的传递从值传递变为了引用传递,将父视图和子视图的属性关联起来。这样子视图针对属性的修改,会传递到父视图上;
需要在属性名称前加上一个美元符号$来获得这个值。因为它是投影属性;
下面代码在主视图上添加一个BtnView视图,视图上添加一个按钮,按钮点击后修改isShowText变量。这里的变量通过传入参数与主视图的isShowText进行绑定。绑定到主视图的isShowText变量上。主视图的变量用来决定文本视图的隐藏和显示;
struct BtnView: View {
@Binding var isShowText: Bool
var body: some View {
Button {
isShowText.toggle()
} label: {
Text("点击")
}
}
}
struct ContentView: View {
@State private var isShowText: Bool = true
var body: some View {
VStack {
if(isShowText) {
Text("点击后会被隐藏")
} else {
Text("点击后会被显示").hidden()
}
BtnView(isShowText: $isShowText)
}
}
}
- 按钮在BtnView视图中,并且通过点击,修改isShowText的值;
- 将BtnView视图添加到ContentView上作为它的子视图。并且传入isShowText;
- 此时的传值是指针传递,会将点击后的属性值传递到父视图上;
- 父视图拿到后也作用在自己的属性,因此他的文本视图会依据该属性而隐藏或显示;
- 如果将@Binding改为@State,会发现点击后不起作用。这是因为值传递子视图的更改不会反映到父视图上;
3)@ObservableObject
对实例进行监听,其用处和@State非常相似,只不过必须是对象,而且这个被监听的对象可以被多个视图使用。需要注意用法;
class DelayedUpdater: ObservableObject {
@Published var value = 0
init() {
for i in 1...10 {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)) {
self.value += 1
}
}
}
}
struct ContentView: View {
@ObservedObject var updater = DelayedUpdater()
var body: some View {
VStack {
Text("\(updater.value)").padding()
}
}
}
- 绑定的数据是一个对象;
- 被修饰的对象,其类必须遵守ObservableObject协议;
- 此时这个类中被@Published修饰的属性都会被绑定;
- 使用@ObservedObject修饰这个对象,绑定这个对象;
- 被@Published修饰的属性发生改变时,SwiftUI就会进行更新;
- 这里当value值会随着时间发生改变。所以updater对象也会发生改,此时文本视图的内容就会不断更新;
4)@EnvironmentObject
在多视图中,为了避免数据的无效传递,可以直接将数据放到环境中,供多个视图进行使用,相当于全局数据;
struct EnvView: View {
@EnvironmentObject var updater: DelayedUpdater
var body: some View {
Text("\(updater.value)")
}
}
struct BtnvView: View {
@EnvironmentObject var updater: DelayedUpdater
var body: some View {
Text("\(updater.value)")
}
}
struct ContentView: View {
let updater = DelayedUpdater()
var body: some View {
VStack {
EnvView().environmentObject(updater)
BtnvView().environmentObject(updater)
}
}
}
- 给属性添加@EnvironmentObject修改,就将其放到了环境中;
- 其他视图中想要获取该属性,可以通过.environmentObject从环境中获取;
- 可以看到分别将EnvView和BtnvView的属性分别放到了环境中;
- 之后我们ContentView视图中获取数据时,可以直接通过环境获取;
- 不需要将数据传递到ContentView,而是直接通过环境获取,这样避免了无效的数据传递,更加高效;
- 如果是在多层级视图之间进行传递,会有更明显的效果;
SwiftUI-Demo-仿App Store页面的实现
1)页面整体结构
- 页面整体使用ScrollView + VStack的布局方式;
- 在VStack中定义需要纵向展示的子页面;
2)标题页布局
- iOS15.0之后,SwiftUI推出了一个全新的属性overlay,用以在某一控件上添加子控件;
3)推荐游戏页布局
- 推荐游戏页整体采用TabView + VStack的布局方式;
- TabView提供了一个可以左右分页滑动的列表界面;
- 使用AsyncImage加载网络图片;
- 使用AsyncImage的overlay属性添加子控件;
- 子控件采用HStack布局;
4)其他游戏页布局
- 其他游戏页整体采用VStack布局;
- 列表部分使用LazyVStack的布局方式(也可以使用List);
- 列表item使用HStack布局;