基本知识
时间轴
小组件通过AppIntentTimelineProvider
进行 UI 刷新
struct Provider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
// 添加占位的(选择添加的时候使用)
// todo
}
func snapshot(for configuration: WidgetCfgIntent, in context: Context) async -> SimpleEntry {
// 添加预览的时候会调用, 建议这里进行和timeline方法一样的数据处理。
// todo
}
func timeline(for configuration: WidgetCfgIntent, in context: Context) async -> Timeline<SimpleEntry> {
// 返回每个时间点的数据。
var entries: [SimpleEntry] = []
// 按需添加自己的时间片段
// entries.append(entry)
let nextTimeMin = 20
let nextUpDate = Calendar.current.date(byAdding: .minute, value:nextTimeMin, to: .now) ?? Date(timeIntervalSince1970: Date().timeIntervalSince1970 + Double(nextTimeMin*60)) // 16-50分钟刷新一次, 不能设置时间太小,太小会被系统忽略
return Timeline(entries: entries, policy: .after(nextUpDate))
}
// 推荐配置。
func recommendations() -> [AppIntentRecommendation<WidgetCfgIntent>] {
// todo
}
}
数据共享
与其他扩展一样,小组件可以通过Group 的UserDefault 共享数据。 还可以通过 SwiftData 共享数据。
交互
widgetURL:所有区域
Link: 不同元素
具有交互性的 Tog 或 Button(iOS 16):需要基于 AppIntent (与系统通用的 Intent 共用)参考https://developer.apple.com/documentation/widgetkit/adding-interactivity-to-widgets-and-live-activities
AppIntet 理解
Intent: 提供的一个小功能(通过入参、实现某个功能、返回什么结果),比如打开什么、记录什么
Entity:用来表示 App的内容,提供给 Intent 使用(对这个小功能的抽象数据)。
AppShortcut:用来包装 Intent,使之能被系统或者调用方发现。
APPIntent 如何返回数据
func perform() async throws -> some IntentResult & ReturnsValue<Int> {
// 如果需要返回其他类型,在 Int 处替换。
let returnValue: Int = 1
//在这里添加你要执行的代码
return .result(value: returnValue)
}
APPIntent 的参数通常用@Parameter标记, 支持基本常用值, 也可以继承AppEntity或AppEnum–枚举进行自定义类型。
实现了 AppIntent 的功能默认自动提供给快捷指令使用。注意多语言和相关测试
小组件更新
主 APP 内:通过WidgetCenter刷新
// 通过 kind 刷新某个类型小组件
WidgetCenter.shared.reloadTimelines(ofKind: "com.mygame.gamestatus")
// 刷新所有小组件
WidgetCenter.shared.reloadAllTimelines()
系统刷新:
- 通过时间轴刷新(15~50分钟一次,设置时间小了,系统也会忽略)
- 执行交互型 Intent 后会触发刷新
支持小组件类型
struct DemoWidget: Widget {
var body: some WidgetConfiguration {
// AppIntentConfiguration 为可以配置的小组件
AppIntentConfiguration(kind: kind, intent: WidgetCfgIntent.self, provider: Provider()) { entry in
SwiftUIWidgetEntryView(entry: entry)
.widgetBackground(Color.container) // 添加自定义背景色(扩展方法),注意小组件最底层始终有一个底色。 无法做到像 Apple Home 那样的半透明背景效果(据说是只有系统才能用的 API)
}
.configurationDisplayName("展示名字")
.description("描述")
.supportedFamilies(supportFamilies) // 支持小组件类型。
.disableContentMarginsIfNeeded() // 忽略边距(扩展方法)
}
private var supportFamilies:[WidgetFamily] {
return [.systemSmall, .systemMedium, .systemLarge]
}
private var kind: String {
return "com.xxxx.Widget"
}
}
小组件配置
通过继承WidgetConfigurationIntent 实现。探索 App Intents 的功能更新 – 小专栏
一组一组配置
相关接口:https://developer.apple.com/documentation/appintents/intentitemcollection
struct DeviceDefaultProvider:DynamicOptionsProvider
// 注意这里需要返回 IntentItemCollection, 即分组显示
func results() async throws -> IntentItemCollection<DeviceEntity> {
// return ItemCollection(promptLabel: "----collection") {
// ItemSection("title1", subtitle: "subtitle1", image: DisplayRepresentation.Image(named: "pic_xxxx")) {
// DeviceEntity(deviceID: "111", name: "device111")
// DeviceEntity(deviceID: "112", name: "device112")
// }
// ItemSection("title2", subtitle: "subtitle2", image: DisplayRepresentation.Image(named: "pic_em0xxxx")) {
// DeviceEntity(deviceID: "211", name: "device211")
// DeviceEntity(deviceID: "212", name: "device212")
// }
// }
}
效果图
小组件扩展
import WidgetKit
import SwiftUI
extension View {
/// 统一设置备件
@ViewBuilder
func widgetBackground(_ backgroundView: some View) -> some View {
if Bundle.main.bundlePath.hasSuffix(".appex"){ // 小组件才生效
if #available(iOS 17.0, *) {
containerBackground(for: .widget) {
backgroundView
}
} else {
background(backgroundView)
}
} else {
background(backgroundView)
}
}
}
extension WidgetConfiguration {
func disableContentMarginsIfNeeded() -> some WidgetConfiguration {
if #available(iOSApplicationExtension 17.0, *) {
// 禁用边距
return self.contentMarginsDisabled()
} else {
return self
}
}
}
小组件刷新失败
entities 查询返回数据空
背景:我在主 APP 修改了小组件显示数据源, 同时调用了WidgetCenter.shared.reloadAllTimelines
刷新,但是回到小组件,数据依然没有变化,通过调试,控制台打印如下:
Error getting AppIntent from LNAction: AppIntent has missing parameter value for 'xxxx'. You may need to set a default value in the initializer of your @Parameter, or using the default method on your Query.
No AppIntent in timeline(for:with:)
上面这种情况属于 APP 内主动刷新失效, 但是系统的时间轴刷新有效。
通过反复测试发现,在桌面小组件自定义配置后才发生。
我的配置参数是基于 AppEntity 实现的自定义类型。
发现每次在调用 EntityQuery 的entities(for)
方法,内部返回空数组,都会打印上面的错误。
struct DeviceEntityQuery: EntityQuery, Sendable {
// 通过 ID 查询的会调用, (比如通过配置匹配,提供数据源等)
func entities(for identifiers: [DeviceEntity.ID]) async throws -> [DeviceEntity] {
//具体逻辑:通过 ID 过滤数据。 如果没匹配到,就返回空。
}
}
APP主动刷新小组件 -> 通过自定义配置在 EntityQuery 方法中查询 -> 查询到,触发刷新小组件的生命周期方法,(没查询,比如返回空数组,则不会刷新小组件生命周期方法)
结论:在entities(for)
的返回结果时,不要返回空数组,否则可能无法刷新。
解决方法:如果没有匹配到,不返回空数组,构建对应的 Entity 返回。
其他问题
widgetURL配置
现象:通过 WidgtURL装饰器配置的 URL 跳转异常。
原因:我在最底层配置得了 WidgetURL, 在子视图也配置了 Widget,导致跳转异常. 根据官网文档解释,如果有多层View 使用了 WidgetURL, 状态和跳转将是不可预料的(在 iOS18.0上,多数情况下都响应最底层的 WidgetURL)
解决方案:通过 Link 或者 Button(AppIntent 交互)处理连接。
Link(destination: URL(string: "xxxxx" ) {
// your view
}
Summery 怎么多语言化
使用新的多语言文件 xcstrings
使用占位模式
Get the daylight duration on ${date} in ${location}
使用示例
Summary("Switch \(\.$device) \(\.$isOn)")// 可选, table: "Localizable1")
注意:变量名要一致
Next
将 App 控制扩展到系统级别: 将功能扩展到系统控制,通过类型小组件 AppIntent 的方式。
资料参考
Apple 官方指导:https://developer.apple.com/cn/documentation/widgetkit/
iOS 小组件系列教程:https://juejin.cn/post/7297513663435210771?searchId=2024082914404396F94C65488FE5528F7E
WidgetExamples – github
SwiftUI 控件 Demo – github
SwiftUI 细节学习参考 --肘子的记事本: 各种SwiftUI 控件、布局理解。