经过前两篇的铺垫,你的电脑或许早已饥渴难耐,是时候让我们撸一个 Widget 了。
知识清单:
准备工作:
Demo 代码来源于 WWDC 20 session 10034.
视频里演示主工程是用的 SwiftUI App 模板。当然是用 App Delegate 模板也是可以的,不过在某些交互有所不同。一会儿涉及到咱们在详谈。
1.工程搭建:
工程设置:
从 WWDC Demo 添加示例代码到工程中,完成之后目录结构

然后运行。
2.添加 Widget
选中 xcode -> file -> Target ,然后模板分类选择 iOS,搜索 widget。结果如图

3.Widget配置
不要添加 Configuration 文件
点击取消这项配置就好了。后续我们会专门讨论这个问题
4.运行应用。
运行是经典的时钟显示,这里我就不在截图了。大家看看自己的效果就行了。
开撸
模板代码解析
@main
struct DemoWidget: Widget {
let kind: String = "DemoWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
WidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
@main 系统执行入口函数标志,widget 代码从这里开始执行,与 Widget 相关的配置都在这里。
StaticConfiguration 一个描述没有用户配置 Widget 内容的对象。Widget 显示的所有内容,都是通过它得到。或许你已经猜到了,与之相对的,当然会有用户配置的,这个咱们放到后面讲。

kindApp 分辨 Widget 的标志。用户自定义的字符串,切记要保持唯一。宿主应用刷新某个widget时会用到
provider确定 Widget 刷新的时间轴对象。他会告诉系统 Widget 刷新的时间和内容,但是注意,这里系统会自己做刷新优化,但结果往往不是我们所希望的。
ContentWidget 的内容。其实就是 Widget 视图。上图绿色框
configurationDisplayNameWidget 的名字。 上图红色框
descriptionWidget 的介绍。上图棕色框
supportedFamiliesWidget 支持的样式,支持的样式类型我们后续会讲。上图为小型样式
其中 WidgetEntryView返回 Widget 的UI。这个没什么好说的,大家用SwiftUI正常画就行了
Widget 不支持异步的页面刷新,所以当 Widget刷新时,刷新所需要内容必须可以同步取到。举个例子,如果Widget的内容涉及图片是,刷新时必须保证相关图片已经保存到本地。
经过上面的配置,我们就创建好了我们的 Widget 。接下来我们看下 Widget 的刷新。Widget 的刷新需要一条时间轴,时间轴是一连串遵守TimelineEntry协议的对象组成,我在这里称他们为组成时间轴节点。OK,我们一块来看下时间轴的 Provider,他的作用是按照一定的规则返回时间轴节点,提供给系统刷新Widget
struct Provider: TimelineProvider {
// Widget 显示时首先会从这里获取时间轴条目,然后渲染出UI的轮廓
// 这里必须立刻返回数据,不能有过长时间操作。不然Widget在有数据返回时一直是黑的
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date())
}
//当你看到 Widget时 系统调用此方法获取当前正在渲染的时间轴条目
//在添加时 Widget 或者 智能叠放中滚动到 Widget 时
//首次Widget显示时会在 getTimeline completion 之后调用
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date())
completion(entry)
}
// 提供一组时间轴条目。可以是当前时间也可以将来的时间。
// 并且指定下次获取时间轴节点的策略
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
// Generate a timeline consisting
// User Custom
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
Widget 配置,内容(其实就是UI),刷新时间轴就是Widget的全部。是不是很简单?当然,勤奋爱撸的你绝对不仅限这些基本操作,或许接下来讲的对你更加有用。
支持的小组件类型:(widget family)
/// A small widget.
case systemSmall
/// A medium-sized widget.
case systemMedium
/// A large widget.
case systemLarge
不同小组件类型返回不同UI:
struct WidgetEntryView: View {
var entry: Provider.Entry
@Environment(\.widgetFamily) var family
@ViewBuilder
var body: some View {
switch family {
case .systemSmall:
// your ui
default:
// your ui
}
}
}
所有类型的Widget UI背景均不能透明。
时间轴取新节点策略:
.never:永远不主动获新取新的时间轴节点
.atEnd:时间轴节点刷新完成后主动调用getTimeline,获取新的时间轴节点
.after(date):结果某个时间点后主动调用getTimeline,获取新的时间轴节点
Widget 刷新时间由系统统一决定,如果需要强制刷新Widget,可以在 App 中使用 WidgetCenter 来重新加载所有时间线:WidgetCenter.shared.reloadAllTimelines()
实际开发过程中, Widget 刷新的最短时间是5分钟,苹果推荐的是15分钟,所以设置获取时间轴节点时要注意时间间隔。作者当时被产品要求每一分钟刷新一次,难受的不行,还好苹果把这事说清楚了。至于准确的刷新时间如何确定,苹果没有在文档指出。
与宿主App交互
Widget 通过深度连接(Deeplink)与宿主应用进行交互
widgetURL 打开应用时,传递 URL 给宿主应用。作用在整个Widget,不能设置UI中的 某个元素
Link允许不同的UI元素有不同的响应,传递不同的URL给宿主应用。
注意:systemSmall 类型仅能支持 widgetURL 交互
widgetURL:
var body: some View {
Stack{
your ui
}
.widgetURL(URL(string: "Your custom URL"))
}
Link:
var body: some View {
Stack() {
Link(destination: "Your custom URL1") {
Stack {
// your ui
}
}
Link(destination: "Your custom URL2") {
Stack {
// your ui
}
}
}
}
宿主App接收响应的URL稍微有点复杂,这里应用生命周期管理不同,开发语言不同,响应的方法会也会有所不同
SwiftUI:
var body: some View {
some view {}
.onOpenURL(perform: { (url) in
// your reponse
})
}
AppDelegate:
//Swift
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {return true}
//Objective-C
-(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options{return YES;}
SceneDelegate:
//Swift
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {}
//Objetive-C
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts { }
创建多个Widget
@main
struct Widgets: WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
Widget1()
Widget2()
}
}
注意:最多创建5个widget
相知不易,Widget 也是一样。下一篇我们分享下异步内容处理,IntentConfiguration相关知识和与宿主的数据共享
参考文档:
timelineprovider
WidgetKit won't request a new timeline at the end
https://developer.apple.com/documentation/widgetkit/timelineprovider
https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date