SwiftUI 0 从头开始
终于在Swift推出的第六个年头,我开始着手准备将iOS开发技术栈全面从OC迁移到Swift上来,这个决定有些迟缓,但确实到时候了。
之所以说到时候了,是基于这样几件事的判断:
- 手机淘宝iOS端开始全面迁移Swift,从底层重构摒弃OC;
- WWDC2020之后,SwiftUI成为了Apple全产品平台的跨平台解决方案;
- WWDC2018之后,出现了数个仅支持Swift的标准库,并且连续两年OC的优化都是底层优化,变相为Swift服务;
- 国外的OC的热门第三方类库基本停更,很多OC问题在Stackoverflow上变得无人问津;
- Swift第三方支持逐渐成熟,开源社区活跃,并且有重量级的服务器程序和机器学习项目出现;
- Swift从5.0开始ABI稳定;
- 最后也是最重要的一点,对于个人开发者和小团队来说,OC无论通过什么构架和黑科技加持,生产效率都无法和最新的Swift技术栈抗衡,SwiftUI的效率更是碾压之前所有iOS上原生UI的开发门道;
但不得不说,国内的Swift学术氛围真的不够好,没有社区。
谨以此文记录我这个写了十年ObjectiveC的Swift初学者在实际上手Swift以后遇到的问题,排名不分先后。
0 struct有什么特点
Swift的struct(结构体)跟OC/C中的struct,不是同一个东西,在Swift中结构体非常常见,常见到基础类型String(字符串)和Array(数组)都是结构体定义的,SwiftUI中的View(视图)也是结构体定义。Swift中结构体,涵盖Class(类)的大部分功能,但有三点显著区别(我目前只发现这些,肯定还有别的):
- 结构体赋值拷贝本体,类赋值是拷贝指针
- 结构体不能继承
- 类不允许有没有初始化的属性,结构体允许有
1 SwiftUI如何和现有项目混编
首先OC是不能直接引用SwiftUI,因为OC没有这个类库,所以要桥接Swift使用;Swift在引入SwiftUI库以后,使用UIHostingController
来初始化并持有SwiftUI页面,下面代码来自SwiftUI工程自带的代码。
|
|
2 some关键字有什么作用
|
|
官方模版中的这段代码,body的类型是不是View,而是some View,这里View是Protocol,some表示不透明结果类型,具体作用我还要再研究研究,待补全。
3 SwiftUI中的List、Form有什么区别
感觉From就是Grouped外观的List,其他没什么区别。
4 ForEach循环数组要怎么写
ForEach循环数组要求要有hash值作为唯一标志,写法如下
|
|
这里的数组中的group结构体是这么定义的:
|
|
id: \.id
部分的写法就是设置hash的参数,这个东西很像Vue和小程序中For循环的设置的id。
5 @propertyWrapper 是什么关键字
@propertyWrapper
关键字紧跟着的结构体,是定义了一种属性包装器,这种包装器的结构是固定的,包装好的属性,可以在其他结构体或者类做声明用,以简化代码。
其实@
符号在ObjectiveC中非常常见,在Swift中也不少,尤其是SwiftUI中出现了大量的这种标记:@State,@Binding,@EnvironmentObject等等。
先说结论,@propertyWrapper可以理解为setter和getter的封装结构体,详细用法可以参考这篇文章(属性包装)[https://www.cnswift.org/properties#spl-8],内容实在是多,我暂时还没研究个彻底明白,就先不多讨论了。
就只从网上一段操作Userdefault的代码稍加分析:
|
|
上面这段是一个@propertyWrapper的简单应用,是网上流传很广的一段代码。要注意的一点是,属性包装器中除了wrappedValue
属性外,其他都不是必须属性。
wrappedValue
就是包装器包装的实际内容,在上面的例子中,init方法记录了初始化的key和defaultValue,用来配合UserDefault使用。wrappedValue
则是通过set,get封装成了计算属性。
这样写的好处就是将下面UserSettings
实例的username属性和UserDefault的UserName
键值对实现了绑定,隐藏了操作UserDefault的细节,方便重复实现类似的逻辑。
这里的UserDefault包装器,使用范型来兼容不同类型的参数,比如给UserSettings增加一个名为Sort的Bool型属性,就可以这么写:
|
|
同样地,UserSettings的sort属性和UserDefalut存储的Sort字段关联了起来,而不用重复写UserDefualt的操作逻辑。
最后说一下ObservableObject
协议和PassthroughSubject
类,这两个东西都是Combine框架提供的,说实话对于刚开始学Swift的我来说,这个头文件真的看的一头雾水,因为涉及了太多的不懂的写法。简单来看Demo的话,objectWillChange明显是用来通知SwiftUI的页面刷新用的,而ObservableObject协议则于SwiftUI的View中定义@ObservedObject关键字搭配使用,例如:
|
|
6 ContentView_Previews是做什么用的
ContentView_Previews在SwiftUI刚出的时候,是用DEBUG宏包裹的,最后正式版出来把宏去掉了。具体作用就是用来绘制每一个SwiftUI文件的实时显示页面。
因为UI整体是由很多部件拼组的树形结构,数据在每个层级需要贯穿始终,在写一些部件时候真实数据满足不了UI测试的方方面面,甚至可能开发时候还没有真实数据,这时候就可以通过在PreviewProvider里面给SwiftUI的页面组件设置测试数据,包括设置@State,@Binding这些关键字修饰的属性。例如:
|
|
在这个例子里面,SelectorListView_Previews结构体就给我们制作的SelectorListView提供了sections和value属性的测试数据。在实时浏览中就可以看到测试数据,这样方便我们在开发阶段进行完备的UI预测,多说一句xcode的实时预览支持各种设备和模式以及系统设置的搭配测试,也支持真机实时测试。
7 如何用控制语句控制View渲染
我在实践中遇到过用if和swifch两种控制语句渲染的情况,在5.1里面是不支持直接渲染控制的,WWDC2020以后好像新版本支持了,但正式版没出我也没有尝鲜。目前的替代解决方案是将控制语句放到func里面返回实际View。举个例子:
|
|
这样一段在SwiftUI里面会报错的,没法这样控制渲染,变通的办法是写成func:
|
|
要注意一点,func的返回值,可以是Text,但这里我是用的AnyView,是为了防止返回的View种类不一,这里AnyView相当于是个可以给任何类型View包装的外壳。不过在上面例子里只返回一种Text的情况下,可以把AnyView的包装去掉。
另外这种写法有隐患,比如body的第一层级要求有且只能有一个View,写两个编译器会报错,不写也会报错。但是用func返回View的写法,可以骗过编译器,这样如果返回的View是nil,会导致程序BUG,所以使用时候要谨慎。
我这还有一个Swifch控制流的Demo供参考,这里就是返回不同类型的View:
|
|
8 如何理解双向绑定 @Binding
双向绑定不是什么新概念,ObjectiveC时代,ReactiveCocoa框架里有个RACChannel类专门负责实现这种效果。
单向监听就是我们熟悉的概念KVO(Key-Value-Observer)模式,而双向就是属性和Control挂钩,双向监听,任何一方值发生变化都一同改变。
先一句话简单说一下双向绑定的优势:极大减少UI的状态总数,以提高UI可预见性,减少不可知问题。
举个简单的完整例子:
|
|
这是一个简单的两个输入框,一个确认按钮的登录UI,实现了必须用户名和密码都输入按钮才可以按的逻辑。使用到双向绑定的部分是TextField("用户名", text:$name)
,注意name变量前面的$符号。在按钮的action里面我们将带$符号和不带的参数对比,就会发现,前者是Stirng类型,后者是Binding<String>
类型,所以$符号是把普通属性简易包装成Binding结构体的缩写。
查看TextField接口,就会发现他的text参数要求传入的就是Binding<String>
结构体。效果是在TextField内部进行了双向绑定的处理,修改TextField的内容,绑定属性name会跟随变化,反向操作也是同步的。
然后我们说一下有双向绑定支持的好处,首先是数据驱动UI的构架减少了UI的状态量,上面这个简单UI,两个输入框(下称AB)有四种情况,分别是:AB都没有内容,AB都有内容,A有内容B没有,B有内容A没有。如果按照数据驱动的理念,只需要两个状态属性来标记这四种情况,实际就是状态数的以2为底的对数值;当输入框有三个,UI的状态数就是8种状态,是指数级增长,而数据驱动UI的理念下我们实际关心的属性数量还是3个,是线性增长的。
数据导向设计是不依赖于双向绑定的,没有双向绑定,也可以实现数据驱动UI设计,但有了双向绑定,这种构架实现起来如虎添翼。
SwiftUI最吸引我的地方就是这里了,我用ObjectiveC尝试的各种项目构架,难点都集中在数据如何各种组件中传递的问题上,SwiftUI提供了一个简单可行的方案。Swift是强类型语言,View中各种数据的管道定义都要类型匹配,保证了软件的健壮,如果觉得这样的设计不够灵活,可以将数据通过protocol来定义,这样将大大降低组件之间的耦合。
9 @State、@Binding和@ObservedObject修饰符分别有什么用
这里基本唯一没包括的是@EnvironmentObject,这个我真的目前没用到,但是根据官方教程里不难理解@EnvironmentObject提供的参数是全局生效,不需要在View的层级结构中不断传递,可以在组件中直接声明就使用的参数。
首先这些修饰符,是给View的外层使用的,而并非是给内部使用的。比如在上面例子中:
|
|
这里的name和pwd,用@State修饰,或用@Binding修饰都可以,内部使用方式是一样的,区别在初始化的传参上。@State修饰属性,直接传String就好,而@Binding修饰属性,则要传Banding结构体,这里是String类型,则是Banding<String>
结构体,看到这里如果不懂尖括号是意思的可以看这里Swift泛型。
前面我们说了,在SwiftUI中普通属性转Banding结构体的快捷方式是前面加$
符号,在PreviewProvider中如果要创建的Banding结构体,需要使用.contrast()
静态方法,用来返回一个不能修改的Banding结构体:
|
|
Banding相较于State,就像是管道的标记,告诉外层View:你传进来的参数,如果在我内部被修改,我会通知你。,State则类似于标记这个属性修改只要我组件内部知道就够了,不会通知外部。
然后说一下@ObservableObject,这个修饰和ObservableObject协议是配对使用的,这是一个Class协议,结构体不能使用,所以@ObservableObject修饰的一定是类实例,不会是结构体。
在实现了ObservableObject协议的类内部,使用PassthroughSubject来通知UI更新:
|
|
这个具体原理我还不是特别明白,以后补充。不过从名称不难看出,subject、send这些都近似于设计模式中订阅者模式的名词,应该是一种邮局模式,多方订阅数据,一方发送订阅,多方都可以收到数据,这里就不细说了,现阶段我也只能照猫画虎来使用,因为头文件看不懂(不懂的关键词和写法太多),只能慢慢补充基础知识以后来分析。
10 如何初始化普通的Binding结构体
我在使用中还真遇到了这样的需求,Banding结构体是可以普通初始化的,下面就是一个自定义Binding结构体的例子,有点类似于计算属性设置setter和getter:
|
|
也许目前你在SwiftUI开发过程中用不到这种写法,整个官方教程里也没有这样的写法,是因为确实没必要。但真要用到的时候,确实很有用。
比如你的数据ObservableObject实例,带参数的计算属性,你就必须用func来计算结果,但func在SwiftUI里面不能用$
来包装,所以就要求func本身要定义返回一个Banding结构体,这时候就要用到Banding的普通初始化写法了。
11 Swift没有valueForKeyPath方法,怎么用String获取属性值
如果在Swift里面使用OC对象,还是可以用的OC的接口的,如果不是OC对象怎么办,那就要用到Mirror了,Mirror是Swift4.1引入的功能,可以用来生成一个实例的反射对象,这个反射对象可以用来获取和设置对象的属性值,具体用法如下:
|
|
上面是初始化方法,这里我设置了一个局部变量用来存储mirror。这里牵扯到一个懒加载的写法问题,我们后面专门开一篇讨论。下面是Mirror的使用,这里是搭配前面说的Binding使用的,整段func的功能是通过String类型的key来生成一个对应属性名的Banding结构体,供给SwiftUI使用,如此操作就不用在SwiftUI中将属性名写死,而是通过key来获取,非常灵活:
|
|
mirror.children.enumerated()
枚举中包含attr对象,拥有label和value两个属性,label是类的方法名,value是值,上面方法是根据key来生成banding结构体,所以要用for循环匹配key和label的值。这里label比实际的key名要多一个下划线。
另外因为我这里的用到的属性,都是@propertyWrapper属性包装过的,所以attr.value
要强转类型为包装结构体UserDefault<Bool>
,get返回的是包装体的wrappedValue
,set修改的也是wrappedValue
。
小结
我这个老OC用户学Swift的确有不少新概念需要慢慢理解,一些理念在其他语言中也能窥见一二,Swift语言的设计毋庸置疑是很前卫的,有其他心得我也会继续扩充这个栏目,与各位共勉。