SwiftUI学习笔记 01

2023年3月31日 · 2 years ago

距苹果在2019年WWDC发布SwiftUI已经过去将近4年时间,去年这页Keynote可谓经典永流传:

其实过去十几年,iOS的UIKit一直是苹果发力的方向,反观macOS使用的AppKit,已经多年不做任何更新了。前些年的更新也只是多了一些“安全”限制,带来一两个小features而已。

所以SwiftUI的出现其实对macOS开发者来说无疑是一种福利。当然,苹果的发布会一般都会说在Xcode上点一个按钮,剩下的就magically happend,但苹果的开发者都知道这个magic遇到现实到底有多么难实现。

所以现在使用SwiftUI开发Mac App是什么样的体验呢?我最近在研究这个东西,顺便写出来权当笔记,分享一下。

一、苹果官方Binary的SwiftUI渗透率

根据 @timac 的Blog分析,iOS 16中官方SwiftUI的使用率相比去年又有了很大的提升:

有226个使用了SwiftUI的Binaries,像Air Drop, FaceTime, Health, Podcasts等App都使用了SwiftUI开发。

macOS这边,AppKit依然是大头,但SwiftUI和Catalyst的占比提升明显:

Mac上的Home, News, Stocks 和 Voice Memos 这些App是用的Catalyst做跨平台开发,同时Reminder, Photos, Notes之类的App也逐渐开始采用SwiftUI混合开发了。

苹果官方数年来的实践也帮助SwiftUI框架取得不小的进步,所以现阶段采用SwiftUI进行官方原生控件的App开发是没有任何问题的。

二、Mac App的main入口

相比大家熟悉的applicationDidFinishLaunching delegate入口,Mac App有更多选择。我们既可以通过Main Menu这个xib文件指定入口,也可以作为命令行启动。

现在我们也可以使用New Project的模板创建一个SwiftUI App for macOS,模板自动创建的入口代码大致如下:

其中App Delegate的成员需要我们自己手动创建,这里不再赘述。

通过SwiftUI创建入口的好处是,我们可以享受SwiftUI Modifiers带来的全部好处,包括窗口管理,Menu Commands的语法糖,Shortcuts语法糖等等。

缺点也是显而易见的:现阶段的SwiftUI,无法直接在View里直接访问所属的Window

大部份时候这并不是问题,但如果你希望在View里操控Window,比如修改大小并展示动画,那么由上述方案创建的View就无法实现了。

如果是通过AppKit创建的其他NSWindow,我们可以通过NSHostingView的接口创建一个SwiftUI View,然后再想办法把这个Window传给它,或者View通过回调来操作Window。但 @main 入口自动创建的第一个Window我们是无能为力的。

所以这种情况下我们还是只能退化到采用Main Menu启动App,然后把 @main 交给App Delegate,再创建一个可控的NSWindowController,然后把第一个View用NSHostingView的方式塞进去来实现对Window的操控。

三、如何在SwiftUI的View里找到它对应的AppKit/UIKit实现?

现阶段SwiftUI的控件在iOS会被转成UIKit实现,macOS上则是AppKit。比如List,在iOS是UITableView,macOS是NSTableView。

而SwiftUI为了隐藏复杂性,暴露的接口和属性其实是它们的子集。当我们对UI有比较强的个性化设计需求时,我们不得不想办法获取它的平台实现然后进行操作。

比如我希望在我的Mac App里,List 不显示背景颜色,但是SwiftUI并没有提供这样的接口,那就需要曲线救国了。

SwiftUI-Introspect这个项目,通过给View结构注入一个IntrospectionView作为锚点,通过View的superview或ViewController的parent/children等接口来寻找符合条件的View,非常聪明的做法。

“注入”采用的是SwiftUI的 overlay 接口,并把注入用的IntrospectionView设置为size 0,这样目标View就是自己的前一个兄弟节点(previous sibling node)。注入的IntrospectionView是一个UIView/NSView,遵循UIViewRepresentable/NSViewRepresentable,所以只要我们拿到这个IntrospectionView对象,就能调用对应的superview等接口,再进行类型判断,找到对应的View。

比如我们想针对一个 List 找到它对应的 NSTableView,那么代码可以这么写:

不过正如作者所说,这种做法可能会随着SwiftUI新版本的发布而失效。

这跟我们以前通过Method Swizzling等方式深入修改AppKit/UIKit的私有类所需要承担的风险是一样的。

四、The best way to build an app?

如果这个App采用iOS/macOS原生控件,没有太多定制化需求,那么SwiftUI使用起来无比丝滑,甚至可以跟设计师坐在一起慢慢调UI细节,的确是效率神器。

不过一旦这个App进入到细节打磨阶段,那么隐藏了接口复杂性的SwiftUI将是一大阻碍,需要开发者经验积累与SwiftUI Framework进化的共同努力。我在开发SwiftUI App时不止一次地推翻原有的方案,重新采用AppKit的实现。当然,这也是我学习的必经之路,所以接下来我也想持续将我踩过的坑写下来,一方面给自己记录回顾,另一方面也许读者朋友看了可以少走一点弯路。

那么本期到此为止,我们下期再见。

五、相关链接