iOS14新特性-WidgetKit开发与实践

2020 年 11 月 10 日

iOS14新特性-WidgetKit开发与实践

iOS14 新特性-WidgetKit 小部件 通过桌面编辑添加,将小部件放在 iOS 主屏幕或 macOS 通知中心上,使用户可以随时访问应用中的内容。同时小部件也可以保持更新,因此用户始终可以一目了然地获得最新信息,同时点击区域可以 Deep Link 跳转到主 APP 任意界面中。



在 2020 年苹果发布会推出 Widget 之后,贝壳就第一时间做出了尝试, 期间苹果中国提供了很多支持与帮助,目前已在贝壳和链家 APP 上线。



小部件具有三种不同的大小(小,中和大),可以显示各种信息。用户可以个性化小部件以查看特定于其需求的详细信息,并以最适合他们的方式安排其小部件。



不同分辨率机型,三种卡片的尺寸也不同:



1 如何开发 WidgetKit?


前期准备:


Xcode 12 及 Bate 版, iOS 14 及 Bate 版, 了解 SwiftUI 控件


1.1 创建 Widget


首先,File->New->Target:



有两种配置可供选择:


  • StaticConfiguration: 对于一个没有用户可配置属性的Widget。


例如,显示一般市场信息的股票市场 Widget,或显示趋势标题的新闻 Widget。


  • IntentConfiguration: 对于一个具有用户可配置属性的Widget来说,你可以使用SiriKit自定义意图来定义属性。您使用 SiriKit 自定义意图来定义属性。


例如,一个天气 Widget 需要一个城市的邮政编码或邮政编码,或者一个包裹跟踪 Widget 需要一个跟踪号码。


下图中「Include Configuration Intent」复选框决定了 Xcode 使用哪种配置。选择 Include Configuration Intent 表示支持用户配置;不需要,则不勾选。



1.2 Widget 初始化配置


对象解析


  • kind


识别 Widget 的字符串。


如果包含多个 widget 后可作为唯一的标识符。


  • Provider


符合 TimelineProvider 的对象。


一个符合 TimelineProvider 的对象,它能产生一个时间线,告诉 WidgetKit 何时渲染 Widget。


时间线包含一个你定义的自定义 TimelineEntry 类型。


时间线条目标识了你希望 WidgetKit 更新 Widget 内容的日期。


在自定义类型中包含你的 Widget 的视图需要渲染的属性。


  • Placeholder


一个 SwiftUI 视图,WidgetKit 用来在第一次渲染 Widget。


占位符是您的 Widget 的通用表示,没有特定的配置或数据。


  • Content Closure(内容闭合)


一个包含 SwiftUI 视图的封闭。


WidgetKit 调用它来渲染 Widget 的内容,从提供者那里传递一个 TimelineEntry 参数。


函数解析


1) placeholder


占位视图,在数据加载前展示,在 xcode12 bate3 和 bate4 中有所有所变化:


  func placeholder(in context: Context) -> SimpleEntry {        SimpleEntry(date: Date(), model: LJWidgetModel.preview_widget)    }
复制代码


2) getSnapshot


快照,在添加组件库中展示


func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {        let entry = SimpleEntry(date: Date(), model: LJWidgetModel.preview_widget)        completion(entry)}
复制代码


3) getTimeline


时间轴,控制刷新时机


 func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) {        let date = Calendar.current.date(byAdding: .hour, value:12, to: Date()) ?? Date()        LJWidgetAPI.loadData{ (model, error) in            guard let model = model else {                let timeline = Timeline(entries: [SimpleEntry(date: date, model: LJWidgetModel.preview_widget)], policy: .after(date))                completion(timeline)                return            }            let timeline = Timeline(entries: [SimpleEntry(date: date, model: model)], policy: .after(date))            completion(timeline)        }    }
复制代码


4) SimpleEntry


数据模型,类似 model


struct SimpleEntry: TimelineEntry {    let date: Date    let model: LJWidgetModel}
复制代码


5) WidgetEntryView


主内容,展示区分小中大卡片 ,可根据 family 来区分


struct LJWidgetEntryView : View {    var entry: Provider.Entry    @Environment(\.widgetFamily) var family

@ViewBuilder var body: some View {

switch family { case .systemSmall: let small = entry.model.small LJWidgetSmall(small) .previewLayout(.sizeThatFits) case .systemMedium: let medium = entry.model.medium LJWidgetMedium(medium) .previewLayout(.sizeThatFits)

case .systemLarge: let large = entry.model.large LJWidgetLarge(large) .previewLayout(.sizeThatFits)

@unknown default: Text("unknown") } }}
复制代码


6) Widget


主界面控制器。kind 为标识符


struct LJWidget: Widget {    let kind: String = "LJWidget"    var body: some WidgetConfiguration {        StaticConfiguration(kind: kind, provider: Provider()) { entry in            LJWidgetEntryView(entry: entry)        }      .configurationDisplayName("My Widget")        .description("This is an example widget.")        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])    }}Widget_Previews  struct LJWidgetLarge_Previews: PreviewProvider {    static var previews: some View {        Group {            LJWidgetLarge(LJWidgetLargeModel.preview)                .frame(width: 329.0, height: 345.0)                .previewLayout(.sizeThatFits)                .colorScheme(.light)            LJWidgetLarge(LJWidgetLargeModel.preview)                .frame(width: 329.0, height: 345.0)                .previewLayout(.sizeThatFits)                .colorScheme(.dark)        }    }
复制代码


Preview 预览



界面开发是 SwiftUI (Apple 要求),可参考:https://developer.apple.com/documentation/widgetkit/creating-a-widget-extension


1.3 Widget 刷新机制


  • 自动刷新


如下图,本身维护一个时间轴,在创建时填充不同时间节点,当到达时间节点位置时,触发刷新。after 会在消耗完时间点后,再次填充,保持循环运行。



  • 手动刷新


一种是 push notification 推送来更新 widget,另一种则是客户端内通过调用接口主动 reload。


2 OC 项目和 Swift 混编


如果原有老项目为 OC 项目,想要实现和 Swift 的相互调用,需用到桥接文件,另外针对引入 Swift 依赖库,会引起包体积增加,大小大概 7~8M 左右,可以在 ipa 包内查看:



2.1 Swift 引用 OC 代码


创建 xxx(工程名)-Bridging-Header 头文件, 并在 Build Setting -> Objective-C Bridging Header 设置其路径



这样我们在桥接文件内,通过 import “xxx.h”引用 OC 的组件库,就可以在 Swift 使用了。


2.2 OC 引用 Swift 代码


桥接文件不需要手动创建,系统帮我创建好了,可以查看 xxx-swift,在文件夹查找不到,但是引用头文件后,可以点击进入查看:



3 主 APP 和 Widget 间通信


因为 Widget 为新的 Target 项目,和之前主项目是两个进程的关系,所以如果想要资源共享,比如登录状态值 token, 网络配置等,就需要用到共享区域来实现通信。苹果提供的共享方式有:


方式一:APPGroup 方式


1) 配置好证书,是 Gourp 功能正常使用中,主 APP 和 Widget 保持统一 key



当我们配置完以后,会在文件目录下多出来一个.entitlements 的文件。


2)主 APP 写下数据


//Main App 通过TextField来向共享文件appGroup.txt中写入数据- (void)textFieldDidEndEditing:(UITextField *)textField {   //获取App Group的共享目录    NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.simon.app.test"];    NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"appGroup.txt"];  //写入文件    [textField.text writeToURL:fileURL atomically:YES encoding:NSUTF8StringEncoding error:nil];}
复制代码


3)Widget 内读取 App Group 的共享目录


NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.simon.app.test"];   NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"appGroup.txt"];    //读取文件     NSString *str = [NSString stringWithContentsOfURL:fileURL encoding:NSUTF8StringEncoding error:nil];   self.shareLabel.text = str;
复制代码


方式二:KeyChain Sharing


KeyChain 可将用户信息加密存储在钥匙串中,保证用户信息的安全性;另外多个应用可通过 keyChain 共享用户信息。


1)同样我们需要配置 keychan 的 key, 保持和主 APP 一致,省事的是少了证书的配置



2)通过对钥匙串的读写来操作 (一般由 SAKeychain 库管理)比较方便,注意一点:实现共享的只是在钥匙串的缓存数据,如果一旦加载到内存中,它的修改不受主 APP 的影响了。


4 常见问题


问题 1 . 选择 Intent (widget 配置),会出现 configtion 查找不到


解决:


方法一: 若不使用 Inetent, 在生成 widget 时不勾选即可,避免这类问题;


方法二:见https://developer.apple.com/forums/thread/653910 (在官网提出后,目前没有官方人员回答,不过有其他答案,未验证)


问题 2 . Xcode 12 bate3 运行贝壳的 widget 未显示 preview


解决:Build systems: 选择新的编译方式


问题 3 . 由于 WidgetKit 使用一些 Swift 的新特性,所以版本需要修改成 Swift5.0



问题 4 . Swift 桥接 OC 组件库


解决:


1) 生成 LJShell-Bridging-Header.h 桥接文件,


2)在 Build setting 中找到 Objective-C Bridging Header 设置对应路径 $(SRCROOT)/LianJiaShell/LJShell-Bridging-Header.h


3)将 Build Settings 中的 Defines Module 选项设置为 YES


问题 5 . 引入 OC 组件库报查找不到


解决:在 podfile 中在 LJWidget 的 target 引入对应 pod xxx


问题 6 . 使用 Widget Preview 功能


bate 版本目前发现在 OC 工程下混编情况,prview 无法使用,可以尝试新建个 swift 工程来编写 swiftUI,使用 preview 功能


问题 7 . Error: Multiple commands produce


解决:


方法一:


不使用 New Build System,在 File > Project/Workspace Settings 中的 Share Project/Workspace Settings 里 build system 将 New Build System(Default)切换成 Legacy build system。


方法二:在 target -> Build phase > Copy Bundle Resource 中找到 info.plist,移除


问题 8 . dyld: Library not loaded:


dyld: Library not loaded: /System/Library/Frameworks/WidgetKit.framework/WidgetKit
Referenced from: /var/containers/Bundle/Application/EA42E025-6CFA-4C90-950E-50D28255B4DA/LJShell.app/LJShell
Reason: image not found
复制代码


解决:https://developer.apple.com/forums/thread/126506


5 参考文献


  • WidgetKit:


https://developer.apple.com/documentation/widgetkit


  • SwiftUI:


https://developer.apple.com/tutorials/swiftui/creating-and-combining-views


本文转载自公众号贝壳产品技术(ID:beikeTC)。


原文链接


iOS14新特性-WidgetKit开发与实践


2020 年 11 月 10 日 10:071011

评论 1 条评论

发布
用户头像
哇,图片多点,手机网页浏览这图片总是意外弹出来了😂
2020 年 11 月 13 日 18:40
回复
没有更多评论了
发现更多内容

基于XGB单机训练VS基于SPARK并行预测(XGBoost4j-spark无痛人流解决方案)

黄崇远@数据虫巢

学习 算法

Spring 中不同依赖注入方式的对比与剖析

Deecyn

spring

借助第一性原理开启中台建设

数字圣杯

数据中台 数字化转型

《硅谷革命:成就苹果公司的疯狂往事》读后感

顾强

一杯茶的时间,上手 React 框架开发

图雀社区

Reac

智浪

Neil

后浪 智能时代 智浪

算法工程师的发展路径

Lucien

你竞争我得利之零售变革

孙苏勇

行业资讯

业务信息化操作系统(BIOS)——中台的核心产出物

孤岛旭日

中台 操作系统 企业信息化

反对996,但是选择996是一个怎样的矛盾心态?

顾强

职场 加班

21天养不成习惯,28天也不行。不要痴心妄想。

赵新龙

TGO鲲鹏会 习惯养成

初探Electron,从入门到实践

Geek_Willie

前端 Electron SpreadJS

Flink Weekly | 每周社区动态更新

Apache Flink

大数据 flink 流计算 实时计算 大数据处理

认识数据产品经理(三 成为数据产品经理)

马踏飞机747

大数据 数据中台 数据分析 产品经理

Dubbo集成Sentinel实现限流

Java收录阁

sentinel

高效阅读,成就自我-《麦肯锡精英高效阅读法》读后感

顾强

读书笔记 读书 读书方式

回“疫”录(15):在家SOHO,是你想要的工作方式吗?

小天同学

疫情 回忆录 现实纪录 纪实 远程办公

编程的门槛 - 抄作业的得与失

顿晓

编程门槛 编程思维 动手能力 抄作业

需求是被挖掘还是被创造出来的?

Neco.W

产品 互联网 需求

面向页面的移动端架构设计

稻子

flutter ios android 前端架构 架构模式

终于有一款组件可以全面超越Apache POI

Geek_Willie

前后端分离 服务端 GrapeCity Documents

从波音747学项目管理

顾强

项目管理 读书感悟 沟通

有了容器为什么kubernetes还需要Pod?

架构师修行之路

Kubernetes 分布式 云原生 pod

前浪的经验:区块链软件,一定也要去中心化

Michael Yuan

比特币 区块链 智能合约 以太坊 加密货币

爱是恒久忍耐,又有恩慈

泰稳@极客邦科技

身心健康 心理

编写制度的几点实用建议

石君

制度 编写制度 安全管理

猿灯塔-Phaser 使用介绍

猿灯塔

故障的传播方式与隔离办法

Wales Kuo

“字节”不断“跳动”,卡拉永远 OK?

无量靠谱

字节跳动 诺基亚 危机

高仿瑞幸小程序 08 创建第一个云函数

曾伟@喵先森

小程序 微信小程序 前端 移动

游戏夜读 | 关卡设计为什么难?

game1night

iOS14新特性-WidgetKit开发与实践-InfoQ