距苹果在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的实现。当然,这也是我学习的必经之路,所以接下来我也想持续将我踩过的坑写下来,一方面给自己记录回顾,另一方面也许读者朋友看了可以少走一点弯路。
那么本期到此为止,我们下期再见。