简介
新的 WidgetKit 框架和 SwiftUI 关于 widget 新的 api,创建的 widget能满足 iOS,iPadOS,和 macOS 平台,能放在 Home screen 的任意位置。而且还拥有智能堆栈 Smart Stack ,集成siri的智能化推荐能力,能根据你使用时间,位置等因素,来智能显示组件。
- Widget使用SwiftUI视图显示其内容,可视化能力强
- 一个App 可以提供多种样式的 Widget,可更具不同情况来显示不同类型的Widget,用户可以选择将其最关心的放置在最重要的位置上,以便最方便的获取信息。
- Widget和之前的TodayWidget是一个独立运行的程序,需要在项目中进行App Groups的设置才能使其与主程序互通数据。
- 目前存在的问题,widget不支持连续的实时更新,只能通过timeline来设置时间轴进行更新。
- Widget 只有大中小3种尺寸(systemLarge、systemMedium、systemSmall)。
项目构建
添加Widget到原工程
打开你的 Xcode 工程, 并且选择 File > New > Target,在 Application Extension 中选择 Widget Extension,输入 Widget 的名字,如果 Widget 提供了用户可配置的属性,请选中“ Include Configuration Intent ”复选框。
在这里简单介绍下Widget的配置信息
小部件扩展模板提供了一个符合小部件协议的初始小部件实现。Widget的body属性决定了该Widget是否具有用户可配置的属性。
创建widget时,包含配置意图「Include Configuration Intent」复选框决定了Xcode使用哪种配置。当选择这个复选框时,Xcode使用将使用默认设置进行配置。
Configuration有两种配置可供选择:
1.StaticConfiguration: 对于一个没有用户可配置属性的Widget。「例如,显示一般市场信息的股票市场Widget,或显示趋势标题的新闻Widget。」
2.IntentConfiguration。对于一个具有用户可配置属性的Widget来说,你可以使用SiriKit自定义意图来定义属性。您使用 SiriKit 自定义意图来定义属性。「例如,一个天气Widget需要一个城市的邮政编码或邮政编码,或者一个包裹跟踪Widget需要一个跟踪号码。」
以下代码创建一个常规的StaticConfiguration,不可配置的状态的 Widget:
struct TestWidget: Widget {
let kind: String = "TestWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind,provider: Provider()) { entry in
TestWidget EntryView(entry: entry)
}
.configurationDisplayName("name")
.description("des")
.supportedFamilies([.systemMedium])
}
}
kind:识别Widget的字符串。这是您选择的标识符,应描述Widget所代表的内容。
Provider:符合TimelineProvider的对象。一个符合TimelineProvider的对象,它能产生一个时间线,告诉WidgetKit何时渲染Widget。时间线包含一个你定义的自定义TimelineEntry类型。时间线条目标识了你希望WidgetKit更新Widget内容的日期。在自定义类型中包含你的Widget的视图需要渲染的属性。
Placeholder:一个 SwiftUI 视图,WidgetKit 用来在第一次渲染Widget。占位符是您的Widget的通用表示,没有特定的配置或数据。
Content Closure(内容闭合):一个包含SwiftUI视图的封闭。WidgetKit调用它来渲染Widget的内容,从提供者那里传递一个TimelineEntry参数。
Custom Intent(自定义配置)。一个定义用户可配置属性的自定义意图。
包括显示名称(name)、描述(description)和Widget支持的系列(families)。
Provider 时间线配置(Provide Timeline Entries)
struct Provider: TimelineProvider {
public typealias Entry = SimpleEntry
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date())
}
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 of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
Provider会生成由时间线条目组成的时间线,每个条目都指定了更新小组件内容的日期和时间。
- placeholder :系统让你的 view 自动渲染一个占位图。
- getSnapshot:快照预览为了在小部件库中显示小部件,在你添加widget时候,预览的样式。当部件还没有从服务器获取状态时,Provider通过显示一个空状态来实现getSnapshot方法。
- getTimeline:在请求初始快照后,WidgetKit调用getTimeline(for:with:completion:)向提供者请求一个常规的时间线。时间线由一个或多个时间线条目和一个重载策略组成,告知WidgetKit何时请求后续时间线。
Widget的刷新策略
Timeline里面有三种方式:atEnd,after(date),never
- atEnd: timeline 中最后一个 entry 显示后更新。timelines 方法会重新调用。
- after(date): 指定日期,重新更新timeline。
- never:系统不会自动更新,除非我们主动通过 Widget Center Api 来更新。
保持组件状态为最新(Keeping a Widget Up To Date)
Widget使用SwiftUI视图来显示它们的内容。WidgetKit在一个单独的过程中代表您渲染视图。因此,即使小组件在屏幕上,小组件扩展也不会持续活跃。尽管widget并不总是处于活动状态,但有几种方法可以使其内容保持最新。
为可预测的事件生成一个时间轴
定义widget时,实现一个自定义的TimelineProvider。WidgetKit从你的provider那里获取一个时间线,并使用它来跟踪何时更新widget。时间线是一个TimelineEntry对象的数组。时间线中的每个条目都有日期和时间,以及小组件显示其视图所需的附加信息。除了时间线条目,时间线还指定了一个刷新策略,该策略告诉WidgetKit何时请求新的时间线.
下面是苹果官网给的一个显示角色健康水平的游戏小部件的例子。
当健康水平低于100%时,角色以每小时25%的速度恢复。例如,当角色的健康水平为25%时,需要3小时才能完全恢复到100%。下图显示了WidgetKit如何从provider那里请求时间线,在时间线条目中指定的每个时间渲染小部件。
func getTimeline(for configuration: CharacterSelectionIntent, with context: Self.Context, completion: @escaping (Timeline<Self.Entry>) -> ()) {
let selectedCharacter = characher(for: configuration)
let endDate = selectedCharacter.fullHealthDate
let oneHour: TimeInterval = 60 * 60
var currentDate = Date()
var entries: [SimpleEntry] = []
while currentDate < endDate {
let relevance = TimelineEntryRelevance(score: Float(selectedCharacter.healthLevel))
let entry = SimpleEntry(date: currentDate, character: selectedCharacter, relevance: relevance)
currentDate += oneHour
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
当WidgetKit最初请求时间轴时,provider会创建一个有四个条目的时间轴。第一个条目代表当前的时间(Now),之后是每小时一次的三个条目。在刷新策略设置为默认的atEnd的情况下,WidgetKit会在时间线条目中的最后一个日期之后请求一个新的时间线。当时间线中的每个日期到达时,WidgetKit会调用小组件的内容闭包显示结果。在最后一个时间线条目过后,WidgetKit会重复这个过程,要求提供者提供一个新的时间线。由于角色的健康度已经达到了100%,提供者会以当前时间的单一条目和刷新策略设置为never来回应。在这种设置下,WidgetKit不会要求另一条时间线,直到应用程序使用WidgetCenter告诉WidgetKit请求新的时间线。
当时间线改变时通知WidgetKit
当某件事情影响到小组件的当前时间线时,或者说当你要刷新Widget的时候,App可以告诉WidgetKit请求新的时间线。App可以告诉WidgetKit重新加载时间线并更新小组件的内容。要重载特定类型的widget,使用WidgetCenter
WidgetCenter.shared.reloadTimelines(ofKind: "com.testWidget")
kind参数包含与用于创建widget的WidgetConfiguration的值相同的字符串。当然也只有一个widget的话也可以用reloadAllTimelines。
如果widget具有用户可配置的属性,那么通过使用WidgetCenter来验证是否存在具有适当设置的widget,从而避免不必要的重新加载。
WidgetCenter.shared.getCurrentConfigurations { result in
guard case .success(let widgets) = result else { return }
if let widget = widgets.first(
where: { widget in
let intent = widget.configuration as? SelectCharacterIntent
return intent?.character == characterThatReceivedHealingPotion
}
) {
WidgetCenter.shared.reloadTimelines(ofKind: widget.kind)
}
}
如果只有一个Widget,你可以使用WidgetCenter来重新加载所有widget的时间线。
WidgetCenter.shared.reloadAllTimelines()
Link WidgetUrl
从 Widget 跳转到 App 指定界面,只需要用 SwiftUI Link 的方式,View 的外层包上一个 Link,destination 是设定好的 url,就能实现跳转了。
Link(destination: URL(string: topJump.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)!) {
HStack() {
Spacer()
.frame(width: Len(15))
// titile
Text(topDesc)
.fontWeight(.regular)
.font(.system(size: Len(14)))
.foregroundColor(Color(red: 51/255, green: 51/255, blue: 51/255, opacity: 1.0))
Image(triangleIcon)
.padding(.trailing, 4.0)
.frame(width: Len(8), height: Len(8))
Spacer()
}
}
widgetURL:在View上加一个widgetURL ,URL是设定好的 url,就能实现跳转了
HStack() {
Spacer()
.frame(width: UMEWLen(15))
// titile
Text(newBean.topDesc)
.fontWeight(.regular)
.font(.system(size: Len(14)))
.foregroundColor(Color(red: 51/255, green: 51/255, blue: 51/255, opacity: 1.0))
Image(triangleIcon)
.padding(.trailing, 4.0)
.frame(width: Len(8), height: Len(8))
Spacer()
}.widgetURL(URL(string: topJump.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)!)
Widget bundles
有时候我们会有多个样式不同种类的 Widget,就需要用 @WidgetBundleBuilder 把多个 Widget 放在一起
@main
struct MainBundle: WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
oneWidget()
twoWidget()
threeWidget()
}
}
Intent
在你的Xcode项目中,选择File > New File并选择SiriKit Intent Definition File。点击 "下一步 "并在提示时保存文件。Xcode创建一个新的.intenttdefinition文件,并将其添加到项目中。
Xcode从意图定义「intent definition file」文件中生成代码。要在一个目标「target」中使用这些代码。
-将intent定义文件作为目标的一个成员。
-通过添加 intent 的类名到 target 属性的 Supported Intents 部分来指定要包含在 target 中的特定 intents。
稍后会上传demo和自己遇到的一些坑
[NewWidget]https://github.com/moneyYouCai/NewWidget-iOS14