Widget入门
总结Widget的常规实现方式,主要参考苹果官方文档,并结合自己在实际项目中的一些经验和体会,以备后续查证。
总的来说Widget实现步骤并不复杂,且Xcode会自动生成其中的绝大多数通用流程的文件以及类和方法,在基本了解了Widget的一些系统设定以后,开发者只需要根据需要添加相应的配置即可。对于熟悉SwiftUI和Swift的开发者来说尤其如此。
目录
一:Widget是什么
一句话描述:
在iOS主屏幕或者macOS的消息中心展示与你app相关的概要内容(文档:Show relevant, glanceable content from your app on the iOS Home screen or macOS Notification Center.)
支持Widget的平台:iOS 14.0+、macOS 11.0+ 、 Mac Catalyst 14.0+
概述:
Widget既可以展示最新的概要信息,也可以把用户直接带入app的特定页面。
Widget有三种(小,中,大)大小,可以展示多种信息,用户可以定制Widget并任意排列它们,当把Widget堆叠时,系统会自动轮转它们,以使最有价值的Widget处于顶端。
开发步骤:
实现一个Widget,你需要:
1.为你的app添加一个widget extension。
2.通过timeline provider来配置widget,timeline provider会告诉WidgetKit何时更新widget的内容。
3.通过SwiftUI来展示widget的内容。
4.如果想要widget是用户可配置的,你需要为你的extention添加一个自定义的SiriKit定义,WidgetKit会自动提供定制界面来让用户使用。
二:Widget开发
1.创建Widget Extension
- Xcode打开app项目 选择 File > New > Target
- 从Application Extension组内选择Widget Extension并点击Next
- 输入extension的名字
- 如果想要Widget是用户可配置的,就勾选Include Configuration Intent checkbox复选框
- 点击Finish
一般来说只创建一个widget extension来包含所有的widget,(尽管可以创建多个,比如单独创建需要定位信息的widget, 以使系统单独提示)。
创建成功后,在你的项目目录中与app文件夹同级的位置会多出一个extension文件夹,其中有一个swift文件,就是我们创建widget的主要文件。
提示:
许多通用流程已经在上述swift文件中自动生成,开发者做的更多的是定制配置工作
2.添加配置条目
Widget extension模板提供了一个遵循Widget协议的Widget实现,这个Widget的属性决定其是否可以由用户配置。有两种可选的配置:
- StaticConfiguration:用户不可配置
- IntentConfiguration:用户可以配置,使用SiriKit自定义属性
这两种配置是由上一步的(勾选Include Configuration Intent checkbox复选框)决定的
初始化配置需要提供以下信息:
- Kind:一个唯一字符串(ID),最好可以表示widget的作用
注意:
Kind参数不要与其它Widget重复,建议用公司网址倒叙做前缀。本人遇到过疑似因为命名重复而导致Widget的占位图只显示一个黑色底图的情况
- Provider:一个遵循TimelineProvider协议的对象,它会提供一个timeline来告诉WidgetKit何时渲染widget。一个timeline包含一个你自定义的TimelineEntry类型。TimelineEntry声明了你想要WidgetKit更新widget的时间和你的widget的view需要渲染使用的属性。
- Content Closure:一个包含SwiftUI视图的闭包,WidgetKit引用这个来渲染widget的内容,并传递给视图TimelineEntry的参数
- Custom Intent:自定义Intent来定义用户可配置属性(具体见可配置Widget)
使用modifiers提供额外配置信息,包括:
- Widget名称:configurationDisplayName
- 描述:description
- 支持的大小:supportedFamilies(大、中、小)
//Widget设置 —— 来自苹果官方文档的示例
@main
struct GameStatusWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(
kind: "com.mygame.game-status",
provider: GameStatusProvider(),
) { entry in
GameStatusView(entry.gameStatus)
}
.configurationDisplayName("Game Status")
.description("Shows an overview of your game status")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
注意:
用户至少要在安装后启动一次app,才能使得app关联的widget在系统的小组件列表中展示
3.提供Timeline Entries
Timeline entry提供更新widget的时间和数据。遵循TimelineEntry协议。
WidgetKit会向provider索要占位图,此图会展示在widget列表中(非主屏幕,见下图)。
通过getSnapshot(in:completion:)方法中传入的context参数的isPreview变量可以判断当前视图是否是占位图。占位图要快速生成,不要占用太长生成时间,可以考虑使用示例数据
//getSnapshot函数实现,提供占位视图 —— 来自苹果官方文档的示例
truct GameStatusProvider: TimelineProvider {
var hasFetchedGameStatus: Bool
var gameStatusFromServer: String
func getSnapshot(in context: Context, completion: @escaping (Entry) -> Void) {
let date = Date()
let entry: GameStatusEntry
if context.isPreview && !hasFetchedGameStatus {
entry = GameStatusEntry(date: date, gameStatus: "—")
} else {
entry = GameStatusEntry(date: date, gameStatus: gameStatusFromServer)
}
completion(entry)
}
之后WidgetKit调用 getTimeline(in:completion:)来向provider请求timeline的规则。Timeline由一个或多个timeline entry组成,同时包含一个说明何时请求下一次timeline的更新规则。下例中展示了包含一个entry并声明更新规则为15分钟后触发下次请求的getTimeline方法
//getTimeline方法包含了timeline entry和15分钟后更新的规则 —— 来自苹果官方文档的代码
struct GameStatusProvider: TimelineProvider {
func getTimeline(in context: Context, completion: @escaping (Timeline<GameStatusEntry>) -> Void) {
// Create a timeline entry for "now."
let date = Date()
let entry = GameStatusEntry(
date: date,
gameStatus: gameStatusFromServer
)
// Create a date that's 15 minutes in the future.
let nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 15, to: date)!
// Create the timeline with the entry and a reload policy with the date
// for the next update.
let timeline = Timeline(
entries:[entry],
policy: .after(nextUpdateDate)
)
// Call the completion to pass the timeline to WidgetKit.
completion(timeline)
}
}
4.提供占位图(主页面)
Widget被首次添加到主屏幕的时候,WidgetKit会使用占位图渲染它,通过调用placeholder(in:)方法来获取占位图信息。
//展示于桌面的占位图 —— 来自苹果官方文档的示例
struct GameStatusProvider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
GameStatusEntry(date: Date(), gameStatus: "—")
}
}
5.展示Widget的内容
使用SwiftUI来定义页面,需要提供你的widget支持的所有大小的页面。
//SwiftUI定义页面 —— 来自苹果官方文档的示例
struct GameStatusView : View {
@Environment(\.widgetFamily) var family: WidgetFamily
var gameStatus: GameStatus
@ViewBuilder
var body: some View {
switch family {
case .systemSmall: GameTurnSummary(gameStatus)
case .systemMedium: GameStatusWithLastTurnResult(gameStatus)
case .systemLarge: GameStatusWithStatistics(gameStatus)
default: GameDetailsNotAvailable()
}
}
}
注意:
Widget展示的是“只读”信息,不支持交互组件(如:滚动控件),WidgetKit会在渲染的时候忽略交互组件
6.添加动态内容
尽管widget是基于你的view的一个截图,但你仍然可以使用各种SwiftUI组件来持续更新它。详见:Keeping a Widget Up To Date。
Widget可以通过Userdefault或者文件系统与app共享数据,使用Userdefault的话,需要在app中添加App Groups,然后使用App Group来初始化Userdefault。
NSUserDefaults *userDef = [[NSUserDefaults alloc] initWithSuiteName:@"group.yourappgroup"];
//初始化UserDefaults
7.用户交互
当用户点击widget,系统会打开你的app。Widget可以通过一个URL来通知app具体跳转到哪个模块。
对于三种大小的widget,他们支持的交互方式不同
小(systemSmall)只支持widgetURL(_:)方式
中(systemMedium)和大(systemLarge)除了支持widgetURL(_:)方式,还可以通过添加Link的方式添加跳转
//通过widgetURL为widget添加跳转交互 —— 来自苹果官方文档的示例
@ViewBuilder
var body: some View {
ZStack {
AvatarView(entry.character)
.widgetURL(entry.character.url)
.foregroundColor(.white)
}
.background(Color.gameBackground)
}
App端通过onOpenURL(perform:), application(_:open:options:), or application(_:open:)等系统方法来获取到widget传入的跳转链接
8.添加多个Widget
为了支持多Widget,你需要添加支持WidgetBundle协议的结构体,在该结构体中把多个widget聚合到它的body属性中
//多Widget —— 来自苹果官方的示例
@main
struct GameWidgets: WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
GameStatusWidget()
CharacterDetailWidget()
LeaderboardWidget()
}
}
9.强制更新Widget
WidgetKit的reloadAllTimelines()方法可以强制重置所有时间线。该方法只能用Swift调用,所以如果主工程是OC写的,就需要进行Swift混编。具体步骤为:
- 主app添加Swift文件,在其中实现调用WidgetKit的reloadAllTimelines()方法的函数
- 根据提示添加Bridging文件
- 在要调用强制刷新的地方引用"appName-Swift.h",其中appName是你工程名,该文件会自动生成,并且无输入提示,直接import即可
- 在要刷新的地方调用第一步中你实现的方法
以上即是初步实现Widget的一些流程。
参考链接:
https://developer.apple.com/documentation/widgetkit/creating-a-widget-extensionhttps://developer.apple.com/documentation/widgetkit/creating-a-widget-extensionWidgets Code-along, part 1: The adventure begins - WWDC20 - Videos - Apple DeveloperTake your app on a most wondrous adventure to the home and Today screens of iPhone, iPad, and Mac. Grab the starter project and code...
https://developer.apple.com/videos/play/wwdc2020/10034/Add intelligence to your widgets - WWDC21 - Videos - Apple DeveloperDiscover how to you can add intelligence to your widgets in Smart Stacks. We'll show you how to use the new Widget Suggestions API in...
https://developer.apple.com/videos/play/wwdc2021/10049/