收录:原文地址
原作者:姜沂(倾寒)
前言
SwiftUI 是苹果公司于 2019 年推出的 Apple Platform 的新一代声明式布局引擎,笔者于去年第一时间升级 Beta 尝鲜全家庭,并在短时间内迅速落地了基于 SwiftUI 的内部 APP, 也分享了几篇关于 SwiftUI 的文章, 但 SwiftUI 1.0 基本没有任何公司敢用在正式上线的主 APP 上,API 在 Beta 版本之间各种废弃,UI 样式经常不兼容,大列表性能差,彼时都标识着 SwiftUI 还称为一个 Toy Framewrok.
随着 WWDC 20 相关新特性和介绍视频的释出,都明确的宣告着 SwiftUI 元年已经到了,SwiftUI 已经成长为新时代的布局引擎。
以下从几个方面分享关于 SwiftUI 的重大改变及核心优势。
PS: 需要读者对 Swift 及 SwiftUI 1.0 有一定熟悉。
SwiftUI Apps
苹果在最近几年的动作中一直在搞 Apple Platform 统一的事情,从最近几年的 iPad 多任务 多窗口,到 Mac Catalyst 再到今年更进一步直接推出了 Apple silicon 芯片更是从硬件上做到了真正统一(话外音:你们在软件上玩的那些跨平台的都是小玩意,硬件才是王道)。
还提供了 Rosetta2 Universal2 帮助开发者基本无成本的迁移到新平台上。但是作为软件工程师还是要更多的关注软件生态的变化。首先了解下创建 APP 时的变化
可以看到创建新工程时有了一套全新的模板基于 SwiftUI App Lifecycle 的跨平台项目。
代码也从原本的基于 UI/NS HostViewController 变成了基于 APP 的声明式描述,下面是代码的前后对比.
- Before
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
}
- After
import SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
其中@main 是Swift 5.1 新增的 Attribute 标记了应用程序的入口点,更多请参看 SE-0281-main-attribute.md
乍看好像只有代码精简了不少,很多人会认为这个简洁程度还不如Flutter 的 main() => runApp(MyApp());.
但最重要的变化是这是第一次跨平台代码,完全无需引入任何 UIKit APPKit WatckKit 等相关Framewok, 即可直接运行在不同平台上。这意味着我们后续在UI布局系统上可以逐渐摆脱对传统命令式 UI 编程的依赖。达到真正的平台无关。
SwiftUI 将整个原有的平台差异部分抽象为 App 和 Scene,对于一个 mac/iOS/iPad/watch/tv/..应用,来说 App 代表了整个应用,Scene 代表了与 Window 相关的多窗口,有些设备只有一个 Scene 有些则有多个,虽然不同的 OS 确实存在差异,但是在语义层面达到了一致。
其次一个没有历史包袱的 APP,也可以完整的从 Swift APP lifecycle 风格式的模板开始,无需再和传统的 UIKit/APPKit 等混合。这也意味着可以达到 APP 完全 Declared and State-Driven。
Viusal Editing
Preview
在传统的利用 DSL 可视编程框架或者平台,诸如 Web Flutter 等技术,都是开发者编写好对应的代码,运行在对应的平台或者调试工具上。SwiftUI 作为苹果最重要的软件层战略框架,更是和 Xcode 深度结合,在运行之前就可以完整的预览你所编写的界面。
强大的 Preview 可以让你既可以从编写 DSL 到立即预览效果,也可从预览的 Canvas 画布中直接修改效果在代码编辑器中生成代码,这对于日常开发的效率有非常大的提高,尤其是在 UI 微调时,效果尤为突出。
Xcode12 可以在 Canvas 上同时预览多个不同设备环境的界面,也可以直接投射到真实的设备上来预览。
对于日常开发来说,编写一个UI界面通常依赖外部的网络/磁盘/其他数据,才能正常的构建,这也造成了UI开发虽然是开发中较为简单的一步,但同时也是最耗时的一步,有了预览功能,可以把很多繁琐的工作前置解决掉,对于研发效率会有非常大的提高。
Xcode Library
在编写真实项目中,一个公司的 APP UI 包含成百上千种风格的 View 组件,对于 UI 组件丰富的产品,如果一个新需求可以由现有的组件组合,那么需求交付的时间也会大大缩短。
但是对于一个大型的开发团队而言,一个开发同学是很难知道公司内到底有多少种组件库,而且即便知道有某种组件库,开发同学初期看到的也是代码,一般需要书写一定的 Demo 才可以用眼睛感知到这个组件到底是否是我想要的。
在 Xcode 12 中提供了更强大的工具,一个自定义组件,只需要遵守一个 LiberyContentProvider 协议就可被Xcode识别,可以像系统控件一样直接从 Xcode 里面识别并预览。对于一个大型团队来说,此功能可以大大提高找寻组件和查看组件样式的效率。
// Without trailing closure:
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
}, completion: { _ in
self.view.removeFromSuperview()
})
// With trailing closure
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
}) { _ in
self.view.removeFromSuperview()
}
// Multiple trailing closure arguments
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0
} completion: { _ in
self.view.removeFromSuperview()
}
DSL
随着 Swift5.3 和 SwiftUI2.0 的推出,SwiftUI 在 DSL 上也更富有表现力, Swift 支持了多重尾闭包语法和在 ViewBuilde 里面支持 Switch Case 语句。
Multiple Trailing Closures
虽然社区对多重尾闭包的讨论上一直存在争议问题,但最终 Swift5.3 还是接受并实现了,在普通命令式编程的地方使用会有一定的困惑性,但是在 SwiftUI 中 DSL 也更有声明式的味道。
// Without trailing closure:
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
}, completion: { _ in
self.view.removeFromSuperview()
})
// With trailing closure
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
}) { _ in
self.view.removeFromSuperview()
}
// Multiple trailing closure arguments
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0
} completion: { _ in
self.view.removeFromSuperview()
}
Switch Case Support
在 SwiftUI 的 ViewBuilder DSL体系中也支持了 Switch case 语法。
var body: some View {
switch c {
case .a:
return Text("A")
case .b:
return Text("B")
case .c:
return Text("C")
}
}
Data Flow
在使用传统命令式编程编写 UI 代码时,开发者需要手动处理 UIView 和 数据之间的依赖关系,每当一个 UIView 使用了外部的数据源,就表明了 UIView 对外部的数据产生了依赖,当一个数据产生变化时,如果意外的没有同步UIView的状态,那么 Bug 就产生了。
处理简单的依赖关系是可控的,但是在真实项目中,视图之间的依赖关系是非常复杂的,假设一个视图只有 4 种状态,组合起来就有 16 种,再加上时序的不同,情况就更加复杂。
人脑处理状态的复杂度是有限的,状态的复杂度一旦超过人脑的复杂度,就会产生大量的 Bug,并且修掉了这个产生了新的Bug。
那么 SwiftUI 是如何解决这个问题的?
SwiftUI 的框架提供了几个核心概念:
统一的 body 属性,SwiftUI 自动从当前 App 状态集自动生成基于当前状态的快照 View。
统一的数据流动原语。
关于 SwiftUI 中的 Data Flow 是如何消除视图和状态不一致的,请参考去年撰写的文档 系列文章《深度解读SwiftUI 背后那些事儿》
今年 SwiftUI 2.0 新增的 StateObject 数据流原语让 SwiftUI 在重复创建 View 时避免重复创建 ObservedObject 从而提高 View 重建的性能。
SceneStorage 和 APPStorgae 让一些可持久化的数据变得更加简单且具有语义化。
New Controls
前面提到的,新增的 DSL 语法 SwiftUI App Lifecycle,以及 Xcode Library Preview 其实本质上都是对去年 SwiftUI 1.0 锦上添花的新扩展。
真正重要的是今年新增的各类新控件,其中通过导出来自 Xcode11.5 和 Xcode12.0 beta 版本的 Swift 声明文件,可以观察到整个声明文件从原来的 10769 行增加到 20564行。
新增了约 87 个 struct 16 个 protocol。有了这些丰富的组件才可以更好的构建我们的 APP 。
大列表组件
在任何一款 APP 中都会存在类似大列表组件,如淘宝 APP 里面的某家店铺里面商品列表流,首页的信息流,都是具有超长内容的列表页数据。对于长列表页来说,过长的 UI 页面会导致过多的内存占用,在用户的设备中,内存是最为重要的指标,对于目前国内的 APP 市场,低端手机仍然占据大量的市场,对于这些设备来说,一旦内存超标,APP 就很容易 OOM,这会导致用户体验非常差,在现有竞争关系激烈的市场环境下,体验差意味着会失去用户。
对于传统的命令式编程来说,我们可以主动控制 UITableViewCell 的重用,自建缓冲池等一系列手段去优化我们的 APP 内存占用,但是对于 SwiftUI 1.0 来说,系统提供的控件并没有有效的办法去让我们控制页面的渲染,对于大列表页面就容易出现内存占用过高的问题。
SwiftUI 2.0 推出了 LazyHStack 和 lazyVStack 加上 List 渲染模式默认就是 Lazy 的直接解决了最大的性能问题。
笔者以去年使用 SwiftUI 编写的 Emas App 为例,当列表页(并无大图)加载到 500个时, APP 使用内存已经达到了将近 360MB 。而只需要切换到 Xcode12 API 调整为到 LazyVStack 内存占用直接降低 300MB 。
Widget and Clips
苹果与 WWDC 20 推出的 WidgetKit 支持的 API 是 SwiftUI Only,虽然已经可以混合部分UIkit 里面的View,但相信没有历史包袱 最低支持版本为 iOS14 的 Widget 没有人会选择笨重的命令式 API。
同理 Clips 也一样。
这里因为篇幅原因就不做展开,后续会有专门的文章分析相关技术。
Swift & SwiftUI 的机会在哪里?
笔者曾经在公司推动集团升级了基建,支持了 Swift 开发环境也在淘宝落地了一些场景,但是集团内一直有一些质疑的声音, 引入 Swift 到底有什么用?
SwiftUI 又是 N 年后才可以用上的小玩意,Objective-C 不够用吗?现在笔者可以回答这些质疑的声音, Swift 未来的机会在 效率,体验和苹果的技术红利。
效率
从研发效率上来说, Swift 对比 Objective-C 的精简程度不言而喻,笔者在淘宝 APP 上线的模块代码量下降了 40 %。 但更进一步,如果编写 UI 界面从 UIKit 转向了 SwiftUI 代码量直接少了不止一倍。更少的代码意味着更快的交付,在目前竞争激烈的市场会有更多的试错场景。关于使用 UIKit 编写代码转向 SwiftUI 的代码量对比,读者可以参考开源 APP MovieSwiftUI 直观了解。
体验
读者可能比较困惑对于切换语言和框架,对体验看上去没有任何帮助,但事实真是这样吗?
首先引入 Swift 后,由于 Swift 语言设计之初便对安全性列为最重要的目标,Swift的引入会让代码尽可能的减少未定义的行为,减少 Crash 意味着APP的稳定性提高,体验自然更佳。
其次虽然 Swift 同样的语言出于对安全性考虑编译处理的指令会比 Objective-C 更多,但是如果UI部分都用 SwiftUI 来写呢?
更少的代码意味者更小的包大小,目前国内巨头 APP iOS 端 APP 包大小都朝着 200 MB 奔去,如果能减少更多的代码对包大小也可以在 200MB 的限制下承载更多而业务。对用户的体验也有较大的提升。
更进一步由于 Swift 选择使用值类型构建整个APP,值类型的有点在于更扁平化的内联数据结构去分配内存,而不是使用更多间接指针引用,减少了大量不必要的堆内存消耗,意味着整体内存使用量的降低。对整个 APP 的稳定性也有较大的提高。
▐ 苹果的选择
Swift 做为苹果的战略语言已经发展的越来越壮大,自 2019 年 Swift ABI 稳定后,苹果在 Swift 的投入越来越大。我们可以进入 /Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/swift , /Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks , https://github.com/apple 和 https://github.com/swift-server 看到, 自 iOS 13 以来 苹果新增了约 10+ Pure Swift Library , 10+ Open Source Swift Library, 以及针对 144 个公开 Framework,根据 Swift Style 重新设计了 57 个 Framework 的API。
从以下数据:
从 WWDC17 后 苹果已经不再使用 Objective-C 做 Sample Code 演示
https://developer.apple.com/不再更新 Objective-C 相关的文档
WidgetKit 是 SwiftUI only。
App Clips 10M的包大小, SwiftUI 是最合适的框架
开源社区逐步放弃 Objecive-C 如 Lottie。
可以判断,Swift 是未来 Apple 平台的唯一选择,越是有包袱的大厂 APP,从现在还不尽早储备,在未来越会寸步难行。
我们需要做些什么?
✎ Swift我们已经做了什么
一套支持 Swift 二进制的研发环境
300+ 支持了混编的淘系 SDK。
手淘落地了 6 个模块。
集团新增了约 20个 支持 Swift 的APP。
10 多场技术培训。
169+ 语雀知识沉淀。
300+ 工程师的集团 Swift 官方组织。
2 个 技术创新产品
经过去年横向组织大家共同的努力,我们已经已经支持了横向大基建。包括研发环境,工具支撑,沉淀了大量的文档,还有相关的技术课程。
要朝什么方向去努力
目前集团对于 Swift 的呼声越来越高,我们大量的工程师希望的去使用 Swift 。
目前首先要做的事情是依托 Swift 和 SPM 提升我们的开发体验,升级我们的中间件,使业务可以大量的用起来 Swift ,提高我们的研发效率和代码质量。
升级基于 SPM 的新的包管理体系
升级老旧基础库,打磨新一代基建。
引入新的 Swift 特有库 赋能业务。
✎ SwiftUI虽然前文提到了 SwiftUI 的众多优点,包括研发效率,体验的提高,但是在国内的环境中 SwiftUI 也有它致命的弊端
iOS 14 才可放心的使用。
只支持 Apple Platform,这和国内的要支持 Mobile Platform 从理念上冲突。
大型 APP 要解决的是如何部署到低版本操作系统上和安卓平台上,毕竟很多公司还在支持 iOS 9 对于升级到最低支持 iOS 14 好像还需要一个世纪那么漫长,而且国内的设备占比大头还是以 Android 巨多 。
虽然可以看到 Swift 语言也在逐渐支持 Android 平台,但是也看到苹果对于安卓平台的 SwiftUI 并没有太大兴趣。
从体验上 Flutter 远不如 SwiftUI 这种亲儿子效果好, 但对于国内跨端欲望旺盛的市场来说 SwiftUI 还是比不过 Flutter, 不过既然 SwiftUI DSL 层已经基本固定,那么也有可能投入人力直接在低版本操作系统上实现一套自建的 SwiftUI 引擎,或者将 SwiftUI 引擎移植到安卓平台,比如对接 Flutter 或者直接对接 Android Native。
比起 Flutter 引入双端带来的包大小增量和体验不一致的情况, SwiftUI 保留 iOS 平台体验,只侵入一端的选择显然要更好一点。
不过短期内我们可以在 Clips 和 Widget 场景下开始使用 SwiftUI, 毕竟 SwiftUI 快速的开发效率对和较低的包大小占用非常适合这样的场景,我们可以在业务场景中练兵储备我们的 SwiftUI,并积极在主 APP 中尝试。