基于SwiftUI的组件化构建实践
阅读本文需要具备一定的iOS开发经验。
SwiftUI是Apple于2019年WWDC上发布的基于Swift语言构建的全新声明式UI框架,抛弃了之前Autolayout,Storyboard的布局方式,提供了Canvas实时预览功能,以提升UI开发的体验。
背景
某迭代了10年+的大型单体iOS
项目,之前完成了部分功能从Objective-C
向Swift
语言迁移的工作。加上近两年该司推行整个大前端(web + mobile)向MFE
(Micro-Front-End)迁移的策略,由此加入了一个MFE
项目,工作内容是将主页功能剥离成独立的组件。
正篇
作为一个上层业务组件,我们会依赖一些已有基础组件框架。受限于一些历史遗留原因,无法直接使用SPM
(Swift Package Manager)作为项目的依赖管理工具。还是使用了Cocoapods
作为项目的依赖管理工具。
项目结构
作为一个组件化项目,主要的业务代码会以Framework
的方式提供给主工程。同时在开发的过程中会建立一个ExampleApp
方便开发者调试。
这个场景就有两种方案可以选择:
- 为
ExampleApp
与Framework
建立两个不同项目,ExampleApp
通过Cocoapods
以本地源码的方式引入Framework
。 - 将
Framework
和ExampleApp
放在同一个工程文件中,以不同Target
的形式引入。
两种方案各有利弊
方案一:
- 优势:
ExampleApp
与Framework
较为分离,更符合真实的使用场景(接入主App),也是业界的主流选择。 - 劣势:每次新加文件和资源都需要
pod install
一次,经常需要开两个项目工程来调试。
- 优势:
方案二:
- 优势:调试方便,新加文件只需
CMD + B
编译一次,ExampleApp
就能引用到。 - 劣势:创建文件的时候需要小心关注
Target
归属,与真实使用场景有差异。在接入主工程的时候,可能会暴露问题。
- 优势:调试方便,新加文件只需
根据团队成员技能水平,使用习惯,综合讨论采用了第二个方案去构建项目。建立出的项目结构如下图:
通过Embed的方式将Framework
引入ExampleApp
:
开发准备
上一步已经把项目基本框架搭建起来了,现在进入工程准备开发。打开工程,Target
选择ExampleApp
打开Canvas
预览默认的Hello world
界面,all good!
毕竟ExampleApp
只是用来调试的,不接入主工程没那么重要。
接下来将Target
切换成我们的MFE
。
满心欢喜打开预览,由此掉入第一个坑。
Canvas报错,预览无法正常工作
如果Canvas
不能用,那就又回到了以前使用UIKit
盲写UI的时代了。
Check
报错信息:
项目MFE
依赖了一个基础的组件,log的最后一行输出了关键信息,MFE
依赖的基础组件缺失。
遇到依赖组件没有找到的问题,首先想到的是去查DerivedData
中的Build
结果。
通过/Users/{MAC_Account_Name}/Library/Developer/Xcode/DerivedData/TestMFE-etvmtlvxanvppnhldjgozcqkeqdq
路径,打开MFE的DerivedData
的Build
文件,发现SwiftUI项目多了一个Previews文件夹,在这个文件夹下找到MFE
在模拟器中的编译生成文件。
最终路径为:/Users/{MAC_Account_Name}/Library/Developer/Xcode/DerivedData/TestMFE-etvmtlvxanvppnhldjgozcqkeqdq/Build/Intermediates.noindex/Previews/TestMFE/Intermediates.noindex/Debug-iphonesimulator
。
在这个路径下我们看到的文件结构是这样的:
可以看到MFE
依赖的SwifterSwift
库,并没有在TestMFE.framework
的文件目录下。我们尝试手动将SwifterSwift
放到TestMFE.framework
文件下。
然后重新启动Canvas
, 预览成功!
依据这个思路得出
解决方案一:
在Xcode中的Build phase
中,添加一个脚本,编译完成将依赖库复制到MFE
的Build
文件中去。1
cp -R "$BUILT_PRODUCTS_DIR/dependency/dependency.framework" "$BUILT_PRODUCTS_DIR/$FRAMEWORKS_FOLDER_PATH"
但该解决方案有个弊端,如果
MFE
只依赖很少的基础组件还好。若需要依赖很多基础组件的话,维护这个脚本就需要很大的成本。那有没有更优的解法?解决方案二:
既然我们使用Cocoapods
进行依赖管理,那通过pod
指定Preview的search path
应该也可以link
到相关的依赖库。
通过搜索Cocoapods
的Git issue找到一种重写pod
的AggregateTargetSettings
, 为Preview link更多search path
的方式。我们需要在Podfile中添加如下描述:
1 | // In Podfile |
Canvas成功预览!
小结:SwiftUI
是一套全新的UI
框架,目前对其Canvas和Preview还不具有很深刻的认识。在解决上述预览问题的过程中,猜想Preview是用Xcode工程项目中对开发者不可见的AggregateTarget
来构建的,随着相关文档逐渐完善,希望以后有机会更深入地了解其实现机制。
解决了上述预览问题,打开工程准备开始写demo code, 随即出现了另一个影响开发的问题。
Auto Preview Failure
在理想状态下,我们使用Auto Preview的状态应该是这样的:
然而在项目中我们的实际使用状态,每输入一个字符,就会遇到这个场景:
Auto preview updating paused, 每次更新完UI部分的代码,都需要手动点击resume
来刷新,丢失了此前丝滑的开发感受。
对于这个问题刚开始没有任何思路,却在点击右侧resume
按钮的时候,发现其会重新触发一次Build
流程。
从这个角度入手,尝试查找MFE
在Build
的过程中是否有额外的操作。在Scheme
-Build
-Post-actions
, 发现了一个之前给Xcode文件排序用的脚本。
尝试移除这个文件排序脚本,Auto preview可以正常工作了。那么文件排序的工作该如何处理?
方案一: 放在
Run
-Post-actions
中,这是一个非常直接的解法。然而项目的MFE是一个Framework
工程,没有Main
函数入口,也就不存在Run
这个流程。因此,这个方案并不合适于此场景。方案二:已经了解了Auto preview被pause是由于在
Build
前后插入了其他操作。那只需找到一个合适的位置插入对应的操作。在Xcode工程里面,哪里最适合做这些额外的脚本操作? –Build Phases
。按照这个思路将文件排序的脚本放到
Compile
之后,Link Binary
之前:
结果失败。尝试将脚本移动到
Link Binary
之后,依然不work。行进至此,感觉进入了死胡同。绝望之中寄希望于
stack overflow
,找到了关于脚本执行的解决方法。通过环境变量,判断Preview的状态,在Preview结束之后再去执行脚本中的操作。改动后的脚本如下:
问题解决! Auto preview works well!!
小结:
在项目工程搭建的过程中,遇到的两个问题都是和SwiftUI
的Preview有关。对Preview
目前的了解尚浅,经历了这个过程至少知道了Preview
其实也隐式的执行了Build
的流程。
后续
解决了影响开发效率的问题,项目搭建基本完成。然而功能的开发才刚刚开始……