SwiftUI的基础与应用

平安喜乐

什么是SwiftUI?

SwiftUI是2019年WWDC大会上,苹果发布的基于Swift语言构建的全新UI框架,开发者可通过它快速为所有的Apple平台创建美观、动态的应用程序;

SwiftUI的运行速度优于UIKit,他减少了界面的层次结构,因此可以减少绘制步骤,并且他完全绕过了CoreAnimation,直接进入Metal,可以有优秀的渲染性能;

SwiftUI 就是⼀种声明式的构建界面的用户接口工具包;

SwiftUI使用声明式的语法构建UI,我们只需要向系统声明UI的View样式,以及View如何转换状态,其他的过程都交给系统去处理;

声明式语法和指令式语法的区别:

  1. 声明式的我们需要提前声明好每个view的各种状态,以及状态转变的条件。后续界面和用户在互动时,系统会帮我们自动进行状态切换;

  2. 指令式的我们需要给每个view先设置好默认状态,后续界面和用户在互动时,需要通过指令不停的去转变view的状态;

  3. 因此声明式的UI是提前声明好各种状态,系统会自动帮我们进行状态切换。指令式的UI是通过我们设定的指令来转换状态;

  4. 比如界面调整、用户交互、机型适配,UIKit都需要手动调整view,对于SwiftUI我们只需要声明好我们想要的样式,系统会帮我们去调整view;

  5. 总结起来,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页面的实现

image.png
1)页面整体结构
image.png
  • 页面整体使用ScrollView + VStack的布局方式;
  • 在VStack中定义需要纵向展示的子页面;
2)标题页布局
image.png
  • iOS15.0之后,SwiftUI推出了一个全新的属性overlay,用以在某一控件上添加子控件;
3)推荐游戏页布局
image.png
  • 推荐游戏页整体采用TabView + VStack的布局方式;
  • TabView提供了一个可以左右分页滑动的列表界面;
  • 使用AsyncImage加载网络图片;
  • 使用AsyncImage的overlay属性添加子控件;
  • 子控件采用HStack布局;
4)其他游戏页布局
image.png
  • 其他游戏页整体采用VStack布局;
  • 列表部分使用LazyVStack的布局方式(也可以使用List);
  • 列表item使用HStack布局;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容