简介
从iOS16和watchOS9开始,WidgetKit 小组件同时能够在iPhone锁屏和watch表盘上展示。显示与你的App最相关的,可浏览的内容,并让人们快速访问App的更多细节。 屏幕小组件和watch表盘应用使用WidgetKit和SwiftUI创建和开发,使我们能够:
- 更新现有的 iOS 主屏幕和watch上今日视图小部件的代码以支持 iPhone 上的锁屏小部件。
- watchOS 应用程序中使用WidgetKit替换ClockKit,让我们的iOS和watchOS 应用程序之间复用更多的代码
- 可以创建同时支持iPhone 锁屏和watch小组件
- 在应用程序中添加对iOS或者watchOS的支持,并创建小组件
Widget种类
WidgetFamily.accessoryCircular (iOS和WatchOS都支持)
WidgetFamily.accessoryRectangular(iOS和WatchOS)
WidgetFamily.accessoryInline(iOS和WatchOS)
WidgetFamily.accessoryCorner(仅支持WatchOS)
Widget创建
- File - New - Target
- 选择 Widget Extension
- 输入Widget Target信息
相关教程 Creating a Widget Extension and SwiftUI
具体Demo
- 地址(github.com/hefeijinbo/…)
- 最后效果
Main App配置
- 配置主App和Widget App的App Group
- App Group用于在Main App和Widget App间共享数据
- Main App保存数据到group user default
Widget App配置
- 配置入口
- 从Group User Default读取数据
- Rectangular组件
// 为小组件展示提供一切必要信息的结构体
struct MyWidgeRectangularProvider: IntentTimelineProvider {
// 占位视图, 例如网络请求失败、发生未知错误、第一次展示小组件都会展示这个view
func placeholder(in context: Context) -> MyWidgeRectangularEntry {
MyWidgeRectangularEntry()
}
// 快照 编辑屏幕在左上角选择添加Widget 第一次展示时会调用该方法
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (MyWidgeRectangularEntry) -> ()) {
completion(MyWidgeRectangularEntry())
}
// 生成一个事件线,更新数据&&进行数据的预处理,转化成Entry
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let timeline = Timeline(entries: [MyWidgeRectangularEntry()], policy: .atEnd)
completion(timeline)
}
}
// Widget 数据源
struct MyWidgeRectangularEntry: TimelineEntry {
var date: Date = Date()
var weightValue = ""
var heightValue = ""
init() {
weightValue = appGroupWeight
heightValue = appGroupHeight
}
}
private let textDefaultColor = Color(red: 199.0 / 255, green: 203.0 / 255, blue: 231.0 / 255)
// widget 展示视图
struct MyWidgetEntryView : View {
var entry: MyWidgeRectangularEntry
@Environment(\.widgetFamily) var family // 尺寸环境变量
var body: some View {
let imageSize:CGFloat = 20
let mediumFontSize: CGFloat = 12
let bigFontSize:CGFloat = 18
return VStack(alignment: .leading, spacing: 3) {
Image("widget_logo").resizable().frame(width: 58, height: 20).aspectRatio(contentMode: .fit)
HStack {
VStack(alignment: .leading, spacing: 0) {
Text("体重").font(.system(size: mediumFontSize)).foregroundColor(textDefaultColor)
HStack(alignment:.lastTextBaseline, spacing: 0) {
Text(entry.weightValue).font(.system(size: bigFontSize)).foregroundColor(Color.blue)
}.frame(height: imageSize + 5)
}
VStack(alignment: .leading, spacing: 0) {
Text("身高").font(.system(size: mediumFontSize)).foregroundColor(textDefaultColor)
HStack(alignment:.lastTextBaseline, spacing: 0) {
Text(String(entry.heightValue)).font(.system(size: bigFontSize)).foregroundColor(Color.blue)
}
}
}
}
}
}
struct MyWidgetRectangular: Widget {
let kind: String = "MyWidgetRectangular"
var body: some WidgetConfiguration {
let configuration = IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: MyWidgeRectangularProvider()) { entry in
MyWidgetEntryView(entry: entry).widgetURL(URL(string:"widget://")!)
}
.configurationDisplayName(CommonLocalizabledString(key: "widget_data", comment: "My Data")) // 添加组件时组件title
return configuration.supportedFamilies([.accessoryRectangular])
}
}
两个Circular组件
// 为小组件展示提供一切必要信息的结构体
struct MyWidgetCircularProvider: IntentTimelineProvider {
let index: Int
init(index: Int) {
self.index = index
}
// 占位视图, 例如网络请求失败、发生未知错误、第一次展示小组件都会展示这个view
func placeholder(in context: Context) -> MyWidgetCircularEntry {
MyWidgetCircularEntry(index: index)
}
// 快照 编辑屏幕在左上角选择添加Widget 第一次展示时会调用该方法
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (MyWidgetCircularEntry) -> ()) {
completion(MyWidgetCircularEntry(index: index))
}
// 生成一个事件线,更新数据&&进行数据的预处理,转化成Entry
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let timeline = Timeline(entries: [MyWidgetCircularEntry(index: index)], policy: .atEnd)
completion(timeline)
}
}
struct MyWidgetCircularEntry: TimelineEntry {
var date: Date = Date()
let index: Int
init(index: Int) {
self.index = index
}
}
@available(iOSApplicationExtension 16.0, *)
struct MyWidgetCircularWidgetEntryView : View {
var entry: MyWidgetCircularEntry
@Environment(\.widgetFamily) var family // 尺寸环境变量
var body: some View {
VStack(spacing: 2) {
HStack() {
Image(entry.index == 0 ? "widget_activity" : "widget_switch").resizable().frame(width: 20, height: 20).foregroundColor(Color.black)
}.frame(width: 30, height: 30).background(Color(white: 220.0/255.0)).cornerRadius(15)
Text(entry.index == 0 ? "活动" : "开关").font(.system(size: 10)).lineLimit(1).padding(.zero).frame(alignment: .center)
}.frame(width: 120, height: 120).background(Color(white: 40.0/255.0)).cornerRadius(60)
}
}
@available(iOSApplicationExtension 16.0, *)
struct MyWidgetCircular: Widget {
init() {
}
var index = 0
var kind: String = ""
var body: some WidgetConfiguration {
// widget URL定义点击widget后的回调,在AppDelegate的application:openURL:中收到回调
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: MyWidgetCircularProvider(index: index)) { entry in
MyWidgetCircularWidgetEntryView(entry: entry).widgetURL(URL(string:kind)!)
}
.configurationDisplayName(index == 0 ? "活动" : "开关")
.supportedFamilies([.accessoryCircular])
}
init(index: Int) {
self.index = index
kind = "MyWidgetCircularWidget" + String(index)
}
}
Widget刷新
如果Widget配置没有刷新,需要重启手机来重载下系统SpringBoard桌面进程