弄清 SwiftUI,才看得懂苹果的强大
SwiftUI 于 2019 年度 WWDC 全球开发者大会上发布,它是基于 Swift 建立的声明式框架。该框架可以用于 watchOS、tvOS、macOS、iOS 等平台的应用开发,等于说统一了苹果生态圈的开发工具。
本人最早开始 iOS 开发时选择了 OC(Objective-C,一种编程语言),当时 OC 不但拥有各种知名的第三方库和完善的社区支持,同时 Swift 语言本身都还在不断颠覆性改进中。但当我看了 2020 年 WWDC 关于 SwiftUI 一系列课程之后,便从 Swift 语言的学习开始,逐步了解并掌握 SwiftUI,并果断抛弃了OC,将新项目全部迁移到了 SwiftUI 框架。
SwiftUI 到底有没有苹果宣传的那么理想化?资深的 iOS 开发者是否有必要转型,以及如何转型?SwiftUI 在实际使用的过程中真实体验如何?这些问题就是这篇文章希望探讨的话题。
在最后,我会分享一些自己的学习心得和材料顺序,借开源的精神与大家共同进步。
什么是 SwiftUI
对于 Swift UI,官方的定义很好找到,也写得非常明确:
SwiftUI is a user interface toolkit that lets us design apps in a declarative way.
可以理解为 SwiftUI 就是⼀种描述式的构建 UI 的⽅式。
单单通过描述,大部分人其实很难对抽象的编程方法,和其中的改进有直观的认识。这篇文章也希望通过尽量口语化的叙述,减少专业词汇和代码的出现来降低阅读门槛,让更多人了解计算机科学,了解程序的世界。
下面是我手头正在做的一个项目,定位是一个原生全平台的电子阅读应用,正在使用 SwiftUI 构建用户界面。
用 SwiftUI 构建的界面以及对应的代码
范例中的界面是书本目录的页面,其中包括了底层的导航栏,分割内容的列表,不同栏目的显示内容等。可以看到,代码部分只有很少的几行,就可以构建出一个简洁却清晰地目录页面,用户点击任意的小标题就会跳转到相应的章节。
得益于 SwiftUI 的特性,现在将想法落地事实上确实便捷了很多,这与之前 UIKit 的例子形成了鲜明的对比。
为什么苹果要推出 SwiftUI
SwiftUI 的两个组成部分,Swift + UI,即是这个问题的答案。
Swift:编程语言和体验的一致性
Swift 代表苹果推出的一种现代编程语言。
很多苹果用户之所以喜欢苹果的产品,其中一个原因,是不同产品之间由内而外的统一感和协调感。这一点在硬件层面的感知是最明显的,从早期开始苹果出的硬件就是「果味十足」的。即使是新品迭代或者是开发全新的品类,也一定会带有烙印很深的「果味」工业设计。
仅仅外观的统一还不够,苹果真正追求的是内外一致,也就是体验的统一。
然而工业设计可以交给自家精英团队,系统可以相互借鉴,但用户使用的软件是由广大的开发者自由创造的。让人意外的是,这一点苹果做的也很不错,与别家相比,质量精良是很多人对苹果系统上软件的印象。
为了实现这一目标苹果做了大量不为普通消费者所感知的工作。
在设计上,苹果提供了一整套不断在更新的 Human Interface Guidelines,详细规定了与视觉相关的各个方面。在完成开发,准备上架分发之前,苹果的审核团队会对每一款应用进行审核,根据 App Store Review Guidelines 的条款判断应用是否允许上架 App Store,即使是知名的应用违反规定也是说下架就下架,绝不含糊。对于不越狱的移动设备而言,App Store 是唯一可以安装应用的途径,控制了其中的准入也就等于替整个平台做了筛选。
除了控制终端以外,苹果也在想方设法增加开发者的数量,提升单个应用质量。方式也非常符合第一性思维原则——降低开发的难度。所以先有了 Swift,紧接着又推出了 SwiftUI。苹果希望直接优化语言本身,并统一所有设备的开发体验,让开发者更容易上手,也更容易将心里的想法转化为运行的程序。
虽说在 2015 年推出 Swift2.0 的时候就进行了开源。但这些年 Swift 在后端或是跨平台的发展上并不是非常顺利。提起 Swift,圈内还是会被默认为特指苹果平台内使用的编程语言,地位有些类似 OC 的接班者。其实为了推广这门语言,苹果本身也做了非常多的工作,像是推出SwiftUI,基本就可以看做苹果在推广新语言的过程中一个里程碑式的节点。
SwiftUI 使用了大量 Swift 的语言特性,特别是 5.0 之后新增的特性。Swift 5.1 的很多特性几乎可以说都是为了 SwiftUI 量身定制的,比如 Opaque return types、Property Delegate 和 Function builder 等。
UI:开发的困局
在 SwiftUI 出现之前,苹果不同的设备之前的开发框架并不互通,移动端的⼯程师和桌⾯端的⼯程师需要掌握的知识,有很⼤⼀部分是差异化的。
苹果目前的软件平台
从 iOS SDK2.0 开始,移动端的开发者⼀直使⽤ UIKit 进⾏⻚⾯部分的开发。UIKit 的思想继承了成熟的 AppKit 和MVC(Model-View-Controller)模式,作出了⼀些改进,但本质上改动不⼤。UI 包括了⽤⼾能看到的⼀切,包括静⽌的显⽰和动态的动画。
再到后来苹果推出了Apple Watch,在这块狭小屏幕上,又引入了一种新的布局方式。这种类似堆叠的逻辑,在某种程度上可以看做 SwiftUI 的未完全体。
截止此时,macOS 的开发需要使用 AppKit,iOS 的开发需要使用 UIKit,WatchOS 的开发需要使用堆叠,这种碎片化的开发体验无疑会大大增加开发者所需消耗的时间精力,也不利于构建跨平台的软件体验。
看似简单的界面其实包含多种 UI 元素
即使单看 iOS 平台,UIKit 也不是完美的开发⽅案。
UIKit 的基本思想要求 ViewController 承担绝⼤部分职责,它需要协调 model,view 以及⽤⼾交互。这带来了巨⼤的 sideeffect 以及⼤量的状态,如果没有妥善安置,它们将在 ViewController 中混杂在⼀起,同时作⽤于 view 或者逻辑,从⽽使状态管理愈发复杂,最后甚⾄不可维护⽽导致项⽬崩溃。换句话说,在不断增加新的功能和⻚⾯后,同⼀个ViewControlle r会越来越庞杂,很容易在意想不到的地⽅产⽣ bug。⽽且代码纠缠在⼀起后也会⼤⼤降低可读性,伤害到维护和协作的效率。
SwiftUI的特点
在很多地方都能看到 SwiftUI 针对现有问题的一些解决思路,而且现在的编程思想经过不断以来的演化,也一直就软件工程在开发过程中的各种问题在寻找答案。
近年来,随着编程技术和思想的进步,使用声明式或者函数式的方式来进行界面开发,已经越来越被接受并逐渐成为主流。SwiftUI 不是第一个,也不会是最后一个使用声明式界面开发的框架。
声明式的界面开发方式
在计算机科学的世界内,抽象是一个很重要的概念。从底层的二进制逻辑门,到人类可以阅读和理解的编程语言之间,是由很多层的抽象将它们关联起来的。所谓抽象,简单解释就是通过封装组件,将底层细节打包并隐藏起来,从而明确逻辑降低复杂度。就像把晶体管打包成逻辑门,以及软件工程中的函数对象。在软件开发的过程中,工程师只需负责某个具体功能的实现,而其他人则通过开放的 api 使用该功能。
与曾经的布局方式相比,声明式的页面开发无疑又加了一层抽象。
在 UIKit 框架中,界面上的每一个元素都需要开发者进行布置,期间有不少计算工作,例如长宽的改变或是屏幕可视面积的变化等。这种线性的方式被称为指令式 (imperative) 编程。以一行文字为例,放置在哪个坐标、宽度多少、在哪里换行、怎么断句、字形字号是多少、最终高度多少、是否需要缩小字号来完全显示等,这些都是开发者在制作界面时要考虑和计算妥当的问题。到了第二年,用户可能会换更大屏幕的手机,系统支持动态字体调节等新功能,此时原先的程序不进行适配就可能出现显示问题,开发者就需要回头进行程序的重新调试。
换做 SwiftUI 之后,上述的很多变量就被系统接管了。开发者要做的就是直观的告诉系统放置一个图像,上面加一行文字,右边加一个按钮。系统会根据屏幕大小、方向等自动渲染这个界面,开发者也不再需要像素级的进行计算。这被称为声明式 (declarative) 编程。
对比同一个场景界面的实现
作为常用的列表视图,在UIKit中被称为 TableView,而在 SwiftUI 中被叫做 List 或 Form。同样是实现一个列表,在 SwiftUI 仅需要声明这里有一个 List 以及每个单元的内容即可。而在UIKit 中,需要使用委托代理的方式定制每个单元的内容,还需要事无巨细的设置行和组的数量、对触摸的反应、点击过程等各方面。
在我的另一个早期项目 Amos 时间志中就可以看到,为了绘制主页就需要几千行代码。
单界面的代码量
智能适配不同尺寸的屏幕
除了不同尺寸的屏幕,SwiftUI 还能根据运行平台的区别,将按钮、对话框、设置项等渲染成匹配的样式。由于声明的留白是很大的,当开发者不需要事无巨细的安排好每一个细节时,系统可操作的空间也会变大。
可以想象,假如苹果推出新品例如眼镜,或许同样的界面代码会被展示成与 iPhone 中完全不同的样式。
提高了解决问题时所需要着手的层级,这可以让开发者可以将更多的注意力集中到更重要的创意方面。
链式调用修改属性
链式调用是 Swift 语言的一种特性,就是用来使用函数方法的一种方式。可以像链条那样不断地调用函数,中间不需要断开。使用这种方式开发者可以给界面元素添加各种属性,只要愿意,同样能够事无巨细的安排页面元素的各种细节。
除了系统预制的属性可以调节外,开发者也可以进行自定义。例如将不同字体、字号、行间距、颜色等属性统合起来,可以组合成为一个叫「标题」的文字属性。之后凡是需要将某一行文字设置成标题,直接添加这个自定义的属性即可。
使用这种方式进行开发无疑能够极大的避免无意义的重复工作,更快的搭建应用界面框架。
界面元素组件化
理论上来讲,每一个复杂的视图,都是由大量简单的单元视图构成。但是函数方法可以包装起来,做到仅在有需要的时候进行调取使用。在 UIKit 框架下的页面元素解耦却不太容易,一般都是针对某种特定情境,很难进行移植。有时候可能手机横屏就会让页面元素混乱,就更别论页面元素的组件化了。
不过 SwiftUI 在布局上的特点,却可以便捷的拆分复杂的视图组件。单一的组件不仅可以自由组合,而且在苹果的任意平台上都可以使用该组件,达到跨平台的实现。
一般我个人会将视图组件区分为基础组件、布局组件和功能组件。因为 SwiftUI 的界面不再像 UIKit 那样,用 ViewController 承载各种 UIVew 控件,而是一切都是视图。这种视图的拼装方式提高了界面开发的灵活性和复用性。
响应式编程框架 Combine
在构建复杂界面的过程中,数据的流通一直是指令式编程中相当让人头疼的部分。
在 UIKit 框架下时,会配合 Target-Action 或者 protocol-delegate 模式来交换信息,使用 Key-Value Observing (KVO) 或者 Key-Value Coding (KVC) 来监测变化和读写属性。但即便开发者熟练地使用这些工具,面对日益增长的应用复杂性,掉坑里的可能性还是非常大。因为有太多需要开发者妥善处理的数据流动,例如数据改动后需要通知相关的页面进行刷新,或是让关联数据重新计算等。
像是 React Native 和 Flutter 这样的移动端跨平台方案,由于采用了声明式 UI 的编写方式和严格的数据流动方向,就能够大幅减轻开发者的思考负担。
SwiftUI 很明显也吸收了这些现代的编程思想,在另一个重量级系统框架 Combine 的协助下,实现了单一数据源的管理。
响应式编程的核心是将所有事件转化成为异步的数据流,这刚好就是 Combine 的主要功能。Combine 采用观察者模式,对应多个观察者,可以分别订阅感兴趣的内容。在 SwiftUI 的界面布局过程中,不同的 View 就是观察者,分别订阅了相关联的属性,并在数据发生变化之后就能够自动的重新渲染。
单一数据源
在 WWDC 的介绍视频中,「Source of truth」这个词反复出现,中文可以将这个词理解为单一数据源。
一直以来复杂的UI结构都会创造更为复杂的数据和逻辑管理需求,每次在用户交互,或是数据来源发生变化的时候,能否及时更新相关界面组件,不然就会引起显示问题。
但是在 SwiftUI 中,只要在属性声明时加上 @State 等关键词,就可以将该属性和界面元素联系起来,在每次数据改动后,都有机会决定是否更新视图。这样就可以将所有的属性都集中到一起进行管理和计算,也不再需要手写刷新的逻辑。因为在 SwiftUI 中,页面渲染前会将开发者描述的界面状态储存为结构体,更新界面就是将之前状态的结构体销毁,然后生成新的状态。而在绘制界面的过程中,会自动比较视图中各个属性是否有变化,如果发生变化,便会更新对应的视图,避免全局绘制和资源浪费。
使用这种方式,读和写都集中在一处,开发者就能够更好地设计数据结构,比较方便的增减类型和排查问题。而不用再考虑线程、原子状态、寻找最新数据等各种细节,再决定通知相关的界面进行刷新。