本篇记录我在开发iOS小组件Widget的知识点记录。
周末给自己练手的App加了两个小组件,踩了一些坑最终完成了小组件的开发,效果如下:
左边的小组件主要用于浏览,数据从主App获取;右侧的小组件用于便捷打开编辑功能。
「外显关键数据」和「便捷便捷入口」也是我自己使用小组件体验以来,觉得小组件最优秀的功能点。
关于小组件的文章网上有很多,这篇文章只记录我踩到坑的地方。
一、初识小组件
(一)恶心的编译报错
在给App新增Widget extension之后,十有八九你会遇到这个编译报错:
我把关键信息抽出来,报错内容大致如下:
1 | SendProcessControlEvent:toPid: encountered an error: Error Domain=com.apple.dt.deviceprocesscontrolservice UserInfo={NSLocalizedDescription=Failed to get descriptors for extensionBundleID |
这个报错是什么意思呢?
报错指的是启动Widget时,找不到 extensionBundleID 的内容了。这是什么意思呢?
因为Widget Extension里是可以指定多个小组件,所以我们需要指定一个默认的小组件版本。
那怎么处理呢?
这时我们可以去Google,可以发现处理这个问题有两个步骤:
- 重启手机
- 给scheme设置_XCWidgetKind,指定默认的小组件
然而当你这么处理后,你会发现有时会生效(编译没问题),有时还会有问题,那怎么办?
根据我在网上搜罗到的信息,得出的结论如下:
这个报错指的就是在scheme设置_XCWidgetKind,和是否重启手机无关(不要浪费时间在重启上了)。
所以推荐的处理方法有两种:
- 如果真机报错,可以尝试使用模拟器;反过来也一样。
- 新建的项目工程报错可能性更小,可以新建工程专门写小组件,然后移植到主工程里。
正如Apple论坛上针对这个问题留言所说:”SwiftUI是个Baby,我们应该照顾他”。
2023年05月02日00:46:53 更新:
导致这个报错的原因就是没有指定 widget extension 的 支持iOS机型而已:
(二)小组件的刷新时机
小组件的内容变化都依赖于 Timeline 。小组件本质上是 Timeline 驱动的一连串静态视图。
没错,你看到的一些会跟随时间刷新的小组件,本质上也是一连串静态视图。小组件是怎么做的呢?
正如上图所示,Timeline 是一个以 TimelineEntry 为元素的数组。 TimelineEntry 包含一个 date 的时间对象,用以告知系统在何时使用此对象来创建小组件的快照。也可以继承 TimelineEntry ,加入业务所需要的数据模型或其他信息。
以我开发的秒级刷新组件为例,虽然说它的名字叫做秒级刷新,但我们实际上是一次性计算好60个entry(小组件的数据),entry 就是我们在特定时间需要展示的数据。
发现了吗,这样做就相当于我们提前把未来时间点UI视图要展示的数据提前计算好了,这点和我们平时写App主工程逻辑不同,值得注意。
系统先回调我们来获取 timeline 数据,再在特定时间来回调我们渲染界面,代码示例如下:
1 | func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { |
二、小组件数据共享
(一)App Group归档问题
小组件是Extension,本质上也是一个独立的App,如果小组件想和主工程App共用数据,能做到吗?
答案是可以,可以使用AppGroup来完成主应用与小组件应用的通讯,App Group是很早的接口了,出发点是为了解决跨App的数据共享问题,比如:
1 | 公司的旗下有两个App,当客户已经登录一个App A的情况下,再登录另一个App B时,B不再需要繁琐的登录过程就可以直接使用A已经登录的信息。但是iOS系统下有这么一个安全机制:每个应用都有自己对应的沙盒,每个沙盒之间都是相互独立的,互不能访问(没有越狱的情况下)。 |
既然连跨App的数据共享都能搞定,那么App-Extension使用App Group进行数据共享也是水到渠成了。
在开发时这里有个值得注意的问题:数据归档编码问题。
App和Extension进行数据共享时,假设我们共享的是一个Dictionary字典,我们在App中将字典数据写入:
1 | NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.memory.app"]; |
这里需要注意的是,既然OC我们使用的是[NSKeyedArchiver archivedDataWithRootObject:]将数据进行归档二进制化,那么在Swift读取App Group时,也需要使用NSKeyedArchiver将二进制对象进行取消归档。
因为Swift中我们经常使用PropertyListEncoder将Dictionary对象转换为Data类型,但在这个情况下如果使用PropertyListEncoder去取数据,数据则会异常。
因此在将数据从OC转换为Swift时,你需要使用相同的编码方式来确保数据的一致性。
1 | var reviewCount : Int = 0 |
(二)共享Swift通知
数据共享完成了,接下来我们想实现App和Extension的互相通信,尤其是App -> Extension的通信是非常有必要的,设想这么一个场景:
如果Widget需要使用App的数据进行渲染,那么App中数据发生了变更,是不是就要立刻通知Widget去App重新取数据?不然就会出现Widget数据滞后的情况。
如果你查询资料,会看到网上宣扬使用 CFNotificationCenter 实现跨进程的App通信, CFNotificationCenter 原理上是能做到的。
但经过我的测试,在App->Extension这条通路上,使用 CFNotificationCenter 发出的通知,Extension是收不到的。
所以我们还是回归App文档,看看苹果建议我们怎么搞:
当 App 在前台时,通过 WidgetCenter 可以主动触发 reload 。
这句话是什么意思呢?表征意思是,我们可以创建一个桥梁Swift类(需要共享到App和Extension两个target),在OC项目App工程需要变更时,App主动调用Swift类进行Extension的刷新即可。
1 | 桥梁Swift类 |
1 | OC类 App |
这种方式最简单,效果最好。不过正如我上面强调的,你需要给桥梁Swift类同时加入到两个target中才会生效:
三、审核相关
Widget是依附于主APP之上的,提交的时候一起审核。