关于 axiom-extensions-widgets-ref
为 Apple 平台开发者提供 widgets 与 App Extensions 的 API 参考,涵盖配置、时间线、交互和 Live Activities 等主题。适用于实现和维护各类系统组件的 Swift 开发。
主要功能
- 01标准与可配置 widgets
- 02交互式 widgets 与 App Intents
- 03时间线和刷新策略
- 04Live Activities 与 Dynamic Island
- 05扩展共享数据与生命周期
适用场景
- 01实现主屏幕或锁定屏幕 widget
- 02创建实时活动
- 03在应用与扩展间共享数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-extensions-widgets-ref安装后在 Claude Code 中输入/axiom-extensions-widgets-ref确认已加载。
详细介绍
扩展与小组件 API 参考
概述
本技能提供 Apple 小组件和扩展生态系统的全面 API 参考:
- 标准小组件(iOS 14+)— 主屏幕、锁定屏幕、待机模式小组件
- 交互式小组件(iOS 17+)— 使用 App Intents 的按钮和开关
- 实时活动(iOS 16.1+)— 锁定屏幕和灵动岛上的实时更新
- 控制中心小组件(iOS 18+)— 系统范围的快捷控制
- Liquid Glass 小组件(iOS 26+)— 强调色渲染、玻璃效果、容器背景
- visionOS 小组件(visionOS 2+)— 挂载样式、纹理、距离感知
- App 扩展 — 共享数据、生命周期、权利
小组件是由系统在时间线上渲染的 SwiftUI 归档快照。扩展是与应用捆绑的沙盒可执行文件。
何时使用此技能
✅ 在以下情况下使用此技能:
- 实现任何类型的小组件(主屏幕、锁定屏幕、待机模式)
- 为持续进行的事件创建实时活动
- 构建控制中心控件
- 在应用和扩展之间共享数据
- 了解小组件时间线和刷新策略
- 将小组件与 App Intents 集成
- 在小组件中采用 Liquid Glass 渲染
- 支持 watchOS 或 visionOS 小组件
- 实现 visionOS 挂载样式、纹理或距离感知
❌ 以下情况不要使用此技能:
- 纯粹的 App Intents 问题(使用 app-intents-ref 技能)
- SwiftUI 布局问题(使用 swiftui-layout 技能)
- 性能优化(使用 swiftui-performance 技能)
- 调试崩溃(使用 xcode-debugging 技能)
相关技能
- app-intents-ref — 交互式小组件和配置的 App Intents
- swift-concurrency — 用于加载小组件数据的 Async/await 模式
- swiftui-performance — 优化小组件渲染
- swiftui-layout — 复杂的小组件布局
- extensions-widgets — 包含反模式和调试内容的专项技能
关键术语
- 时间线(Timeline) — 定义何时显示何种内容的一系列条目;系统会在指定时间显示条目
- TimelineProvider — 提供时间线条目的协议(占位符、快照、时间线生成)
- TimelineEntry — 包含小组件数据和显示日期的结构体
- 时间线预算(Timeline Budget) — 时间线重新加载的每日限额(40-70 次)
- 预算豁免(Budget-Exempt) — 不计入预算的重新加载(由用户发起、应用进入前台、由系统发起)
- 小组件系列(Widget Family) — 尺寸/形状(systemSmall、systemMedium、accessoryCircular 等)
- App Groups — 用于在应用和扩展之间共享数据容器的权利
- ActivityAttributes — 静态数据(设置一次)+ 动态 ContentState(在生命周期内更新)
- ContentState — ActivityAttributes 中会变化的部分;总大小必须小于 4KB
- 灵动岛(Dynamic Island) — iPhone 14 Pro+ 上的实时活动显示;具有紧凑、最小和展开尺寸
- ControlWidget — iOS 18+ 中适用于控制中心、锁定屏幕和操作按钮的小组件
- 辅助活动系列(Supplemental Activity Families) — 支持在 Apple Watch 或 CarPlay 上显示实时活动
第 1 部分:标准小组件(iOS 14+)
小组件配置类型
StaticConfiguration
适用于不需要用户配置的小组件。
@main
struct MyWidget: Widget {
let kind: String = "MyWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
MyWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This widget displays...")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
AppIntentConfiguration(iOS 17+)
适用于使用 App Intents 配置的小组件。
struct MyConfigurableWidget: Widget {
let kind: String = "MyConfigurableWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: kind,
intent: SelectProjectIntent.self,
provider: Provider()
) { entry in
MyWidgetEntryView(entry: entry)
}
.configurationDisplayName("Project Status")
.description("Shows your selected project")
}
}
从 IntentConfiguration 迁移:iOS 16 及更早版本使用 IntentConfiguration 和 SiriKit 意图。迁移到 AppIntentConfiguration 以支持 iOS 17+。
ActivityConfiguration
适用于实时活动(详见“实时活动”部分)。
选择合适的配置
无需用户配置?使用 StaticConfiguration。选项简单且固定?使用带有 WidgetConfigurationIntent 的 AppIntentConfiguration。选项来自应用数据且动态变化?使用 AppIntentConfiguration + EntityQuery。
快速参考:
- StaticConfiguration — 无需自定义(天气、电池状态)
- AppIntentConfiguration(简单)— 固定选项(计时器预设、主题选择)
- AppIntentConfiguration(EntityQuery)— 来自应用数据的动态列表(项目/联系人/播放列表选择器)
- ActivityConfiguration — 正在进行的实时事件(配送跟踪、锻炼进度、体育比分)
Widget 类型
系统类型(主屏幕)
systemSmall(约 170×170,iOS 14+)— 单条信息、图标systemMedium(约 360×170,iOS 14+)— 多个数据点、图表systemLarge(约 360×380,iOS 14+)— 详细视图、列表systemExtraLarge(约 720×380,iOS 15+ 仅限 iPad)— 丰富布局、多个视图
配件类型(锁定屏幕,iOS 16+)
accessoryCircular(约 48×48pt)— 圆形复杂功能,图标或仪表accessoryRectangular(约 160×72pt)— 时钟上方,文本 + 图标accessoryInline(单行)— 日期上方,仅文本
示例:支持多种类型
struct MyWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "MyWidget", provider: Provider()) { entry in
if #available(iOSApplicationExtension 16.0, *) {
switch entry.family {
case .systemSmall:
SmallWidgetView(entry: entry)
case .systemMedium:
MediumWidgetView(entry: entry)
case .accessoryCircular:
CircularWidgetView(entry: entry)
case .accessoryRectangular:
RectangularWidgetView(entry: entry)
default:
Text("Unsupported")
}
} else {
LegacyWidgetView(entry: entry)
}
}
.supportedFamilies([
.systemSmall,
.systemMedium,
.accessoryCircular,
.accessoryRectangular
])
}
}
时间线系统
TimelineProvider 协议
提供用于定义系统何时应渲染 Widget 的条目。
struct Provider: TimelineProvider {
// Placeholder while loading
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), emoji: "😀")
}
// Shown in widget gallery
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), emoji: "📷")
completion(entry)
}
// Actual timeline
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let currentDate = Date()
// Create entry every hour for 5 hours
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, emoji: "⏰")
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
TimelineReloadPolicy
控制系统何时请求新时间线:
.atEnd— 在最后一个条目之后重新加载.after(date)— 在指定日期重新加载.never— 不自动重新加载(仅手动)
手动重新加载
import WidgetKit
// Reload all widgets of this kind
WidgetCenter.shared.reloadAllTimelines()
// Reload specific kind
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")
性能与预算速查
时间线刷新预算
- 每日预算:每天 40-70 次重新加载(因系统负载和使用情况而异)
- 不计入预算:用户发起的重新加载、应用进入前台、添加 Widget、系统重启
- 策略性刷新(每小时 4 次)— 每天约 48 次重新加载,对电池影响较小
- 频繁刷新(每小时 12 次)— 下午 6 点前耗尽预算,影响较大
- 仅按需刷新 — 每天 5-10 次重新加载,影响极小
- 在重要数据变化和基于时间的事件发生时重新加载。避免推测性或仅为美观而进行的重新加载。
// ✅ GOOD: Strategic intervals (15-60 min)
let entries = (0..<8).map { offset in
let date = Calendar.current.date(byAdding: .minute, value: offset * 15, to: now)!
return SimpleEntry(date: date, data: data)
}
内存限制
- 标准 Widget 约 30MB,实时活动约 50MB — 超出时系统会终止进程
- 只加载所需内容(例如,
loadRecentItems(limit: 10),而不是整个数据库)
网络请求
绝不要在 Widget 视图中发起网络请求 — 请求无法在渲染前完成。请改为在 getTimeline() 中获取数据。
时间线生成
在 5 秒内完成 getTimeline()。在主应用中缓存开销较大的计算,从共享容器读取预计算数据,并将条目限制为 10-20 个。
视图渲染
在 TimelineEntry 中预先计算所有内容,让视图保持简单。不要在 body 中执行开销较大的操作。
图像
- 使用资源目录中的图像或 SF Symbols(速度快)
- 可以使用共享容器中的小图像
AsyncImage在 Widget 中无法使用- 大图像会导致内存超限并被终止
第 2 部分:交互式 Widget(iOS 17+)
按钮和开关
交互式 Widget 使用 SwiftUI Button 和 Toggle,并配合 App Intents。
使用 App Intent 的按钮
Button(intent: IncrementIntent()) {
Label("Increment", systemImage: "plus.circle")
}
该意图在其 perform() 方法中通过 App Groups 更新共享数据。完整的 AppIntent 定义语法请参阅 axiom-app-intents-ref。
使用 App Intent 的开关
模式与按钮相同 — 使用绑定到状态的 Toggle,并在状态变化时调用意图:
Toggle(isOn: $isEnabled) {
Text("Feature")
}
.onChange(of: isEnabled) { newValue in
Task { try? await ToggleFeatureIntent(enabled: newValue).perform() }
}
该意图遵循相同的 AppIntent 结构,并包含 @Parameter(title: "Enabled") var enabled: Bool。完整的 AppIntent 定义语法请参阅 axiom-app-intents-ref。
invalidatableContent 修饰符
在 App Intent 执行期间提供视觉反馈。
struct MyWidgetView: View {
var entry: Provider.Entry
var body: some View {
VStack {
Text(entry.status)
.invalidatableContent() // Dims during intent execution
Button(intent: RefreshIntent()) {
Image(systemName: "arrow.clockwise")
}
}
}
}
效果:关联的意图执行期间,带有 .invalidatableContent() 的内容会略微变透明,从而向用户提供反馈。
动画系统
数值文本的 contentTransition
Text("\(entry.value)")
.contentTransition(.numericText(value: Double(entry.value)))
效果:数字会平滑地递增或递减,而不是瞬间变化。
视图转场
VStack {
if entry.showDetail {
DetailView()
.transition(.scale.combined(with: .opacity))
}
}
.animation(.spring(response: 0.3), value: entry.showDetail)
第 3 部分:可配置小组件(iOS 17+)
WidgetConfigurationIntent
为小组件定义配置参数。
import AppIntents
struct SelectProjectIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Select Project"
static var description = IntentDescription("Choose which project to display")
@Parameter(title: "Project")
var project: ProjectEntity?
// Provide default value
static var parameterSummary: some ParameterSummary {
Summary("Show \(\.$project)")
}
}
Entity 和 EntityQuery
为配置提供动态选项。
struct ProjectEntity: AppEntity {
var id: String
var name: String
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Project")
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)")
}
}
struct ProjectQuery: EntityQuery {
func entities(for identifiers: [String]) async throws -> [ProjectEntity] {
// Return projects matching these IDs
return await ProjectStore.shared.projects(withIDs: identifiers)
}
func suggestedEntities() async throws -> [ProjectEntity] {
// Return all available projects
return await ProjectStore.shared.allProjects()
}
}
在 Provider 中使用配置
struct Provider: AppIntentTimelineProvider {
func timeline(for configuration: SelectProjectIntent, in context: Context) async -> Timeline<SimpleEntry> {
let project = configuration.project // Use selected project
let entries = await generateEntries(for: project)
return Timeline(entries: entries, policy: .atEnd)
}
}
第 4 部分:实时活动(iOS 16.1+)
ActivityAttributes
定义实时活动的静态和动态数据。
import ActivityKit
struct PizzaDeliveryAttributes: ActivityAttributes {
// Static data - set when activity starts, never changes
struct ContentState: Codable, Hashable {
// Dynamic data - updated throughout activity lifecycle
var status: DeliveryStatus
var estimatedDeliveryTime: Date
var driverName: String?
}
// Static attributes
var orderNumber: String
var pizzaType: String
}
关键限制:要成功启动,ActivityAttributes 的总数据大小必须小于 4KB。
启动活动
请求授权
import ActivityKit
let authorizationInfo = ActivityAuthorizationInfo()
let areActivitiesEnabled = authorizationInfo.areActivitiesEnabled
启动活动
let attributes = PizzaDeliveryAttributes(
orderNumber: "12345",
pizzaType: "Pepperoni"
)
let initialState = PizzaDeliveryAttributes.ContentState(
status: .preparing,
estimatedDeliveryTime: Date().addingTimeInterval(30 * 60)
)
let activity = try Activity.request(
attributes: attributes,
content: ActivityContent(state: initialState, staleDate: nil),
pushType: nil // or .token for push notifications
)
错误处理
常见活动错误
请求前务必检查 ActivityAuthorizationInfo().areActivitiesEnabled。处理 Activity.request() 的以下错误:
ActivityAuthorizationError— 用户拒绝了实时活动权限ActivityError.dataTooLarge— ActivityAttributes 超过 4KB;请缩减属性大小ActivityError.tooManyActivities— 已达到系统限制(通常为同时运行 2-3 个)
请求成功后,保存 activity.id,以便后续更新。
更新活动
使用新内容更新
// Find active activity by stored ID
guard let activity = Activity<PizzaDeliveryAttributes>.activities
.first(where: { $0.id == storedActivityID }) else { return }
let updatedState = PizzaDeliveryAttributes.ContentState(
status: .onTheWay,
estimatedDeliveryTime: Date().addingTimeInterval(10 * 60),
driverName: "John"
)
await activity.update(
ActivityContent(
state: updatedState,
staleDate: Date().addingTimeInterval(60) // Mark stale after 1 min
)
)
提醒配置
await activity.update(updatedContent, alertConfiguration: AlertConfiguration(
title: "Pizza is here!",
body: "Your \(attributes.pizzaType) pizza has arrived",
sound: .default
))
监控活动生命周期
使用 activity.activityStateUpdates 异步序列观察状态变化(.active、.ended、.dismissed、.stale)。在 .ended 或 .dismissed 时清理已保存的活动 ID。在 deinit 中取消监控任务。
结束活动
消除策略
await activity.end(
ActivityContent(state: finalState, staleDate: nil),
dismissalPolicy: .default
)
消除策略选项:
.immediate— 立即移除.default— 在锁定屏幕上保留约 4 小时.after(date)— 在指定时间移除(例如,.after(Date().addingTimeInterval(3600)))
实时活动的推送通知
请求推送令牌
let activity = try Activity.request(
attributes: attributes,
content: initialContent,
pushType: .token // Request push token
)
// Monitor for push token
for await pushToken in activity.pushTokenUpdates {
let tokenString = pushToken.map { String(format: "%02x", $0) }.joined()
// Send to your server
await sendTokenToServer(tokenString, activityID: activity.id)
}
频繁推送更新(iOS 18.2+)
标准限制为每小时约 10-12 次推送。对于实时事件(体育赛事、股票),添加 com.apple.developer.activity-push-notification-frequent-updates 权限,以大幅提高限制。
第 5 部分:灵动岛(iOS 16.1+)
展示类型
实时活动会以三种尺寸显示在灵动岛中:
紧凑(左侧 + 右侧)
当另一个实时活动展开,或有多个活动处于活跃状态时显示。
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "timer")
}
DynamicIslandExpandedRegion(.trailing) {
Text("\(entry.timeRemaining)")
}
// ...
} compactLeading: {
Image(systemName: "timer")
} compactTrailing: {
Text("\(entry.timeRemaining)")
.frame(width: 40)
}
最小
当有两个以上实时活动处于活跃状态时显示(圆形头像)。
DynamicIsland {
// ...
} minimal: {
Image(systemName: "timer")
.foregroundStyle(.tint)
}
展开
用户长按紧凑视图时显示。
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "timer")
.font(.title)
}
DynamicIslandExpandedRegion(.trailing) {
VStack(alignment: .trailing) {
Text("\(entry.timeRemaining)")
.font(.title2.monospacedDigit())
Text("remaining")
.font(.caption)
}
}
DynamicIslandExpandedRegion(.center) {
// Optional center content
}
DynamicIslandExpandedRegion(.bottom) {
HStack {
Button(intent: PauseIntent()) {
Label("Pause", systemImage: "pause.fill")
}
Button(intent: StopIntent()) {
Label("Stop", systemImage: "stop.fill")
}
}
}
}
设计原则(来自 WWDC 2023-10194)
同心对齐
内容应以均匀的边距同心嵌套在灵动岛的圆角形状内。使用 Circle() 或 RoundedRectangle(cornerRadius:),不要使用尖角的 Rectangle(),否则会凸入角落。
仿生动效
灵动岛动画应显得自然且富有弹性。使用 .spring(response: 0.6, dampingFraction: 0.7) 或 .interpolatingSpring(stiffness: 300, damping: 25),而非线性动画。
第 6 部分:控制中心小组件(iOS 18+)
ControlWidget 协议
控件会显示在控制中心、锁定屏幕和操作按钮中(iPhone 15 Pro+)。
StaticControlConfiguration
用于无需配置的简单控件。
import WidgetKit
import AppIntents
struct TorchControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "TorchControl") {
ControlWidgetButton(action: ToggleTorchIntent()) {
Label("Flashlight", systemImage: "flashlight.on.fill")
}
}
.displayName("Flashlight")
.description("Toggle flashlight")
}
}
AppIntentControlConfiguration
用于可配置的控件。
struct TimerControl: ControlWidget {
var body: some ControlWidgetConfiguration {
AppIntentControlConfiguration(
kind: "TimerControl",
intent: ConfigureTimerIntent.self
) { configuration in
ControlWidgetButton(action: StartTimerIntent(duration: configuration.duration)) {
Label("\(configuration.duration)m Timer", systemImage: "timer")
}
}
}
}
ControlWidgetButton
用于离散操作(一次性操作)。
ControlWidgetButton(action: PlayMusicIntent()) {
Label("Play", systemImage: "play.fill")
}
.tint(.purple)
ControlWidgetToggle
用于布尔状态。
struct AirplaneModeControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "AirplaneModeControl") {
ControlWidgetToggle(
isOn: AirplaneModeIntent.isEnabled,
action: AirplaneModeIntent()
) { isOn in
Label(isOn ? "On" : "Off", systemImage: "airplane")
}
}
}
}
值提供器(异步状态)
对于需要异步状态的控件,请将 ControlValueProvider 传给 StaticControlConfiguration:
struct ThermostatProvider: ControlValueProvider {
func currentValue() async throws -> ThermostatValue {
let temp = try await HomeManager.shared.currentTemperature()
return ThermostatValue(temperature: temp)
}
var previewValue: ThermostatValue { ThermostatValue(temperature: 72) }
}
提供器的值会传入控件的闭包:{ value in ControlWidgetButton(...) }。
可配置控件
使用 AppIntentControlConfiguration 和 WidgetConfigurationIntent(与可配置 Widget 使用相同模式)。添加 .promptsForUserConfiguration(),以便用户添加控件时显示配置界面。
控件调整
.controlWidgetActionHint("Toggles flashlight")— VoiceOver 辅助功能提示.displayName("My Control")/.description("...")— 在控制中心界面中显示
第 7 部分:iOS 18+ 更新
强调色渲染与 Liquid Glass
Widget 渲染模式横跨多个 iOS 版本:widgetAccentable()(iOS 16+)、WidgetAccentedRenderingMode(iOS 18+),以及 glassEffect() 和 GlassEffectContainer 等 Liquid Glass 效果(iOS 26+)。检测当前模式并相应调整布局。
检测渲染模式
struct MyWidgetView: View {
@Environment(\.widgetRenderingMode) var renderingMode
var body: some View {
if renderingMode == .accented {
// Simplified layout — opaque images tinted white, background replaced with glass
} else {
// Standard full-color layout
}
}
}
widgetAccentable(_:)
将视图标记为强调色组的一部分。在强调色模式下,强调色组视图会与主色组视图分别着色,从而形成视觉层次。
HStack {
VStack(alignment: .leading) {
Text("Title")
.font(.headline)
.widgetAccentable() // Accent group — tinted in accented mode
Text("Subtitle")
// Primary group by default
}
Image(systemName: "star.fill")
.widgetAccentable() // Also accent group
}
WidgetAccentedRenderingMode
控制图像在强调色模式下的渲染方式。应用于 Image 视图:
Image("myPhoto")
.widgetAccentedRenderingMode(.accented) // Tinted with accent color
Image("myIcon")
.widgetAccentedRenderingMode(.monochrome) // Rendered as monochrome
Image("myBadge")
.widgetAccentedRenderingMode(.fullColor) // Keeps original colors (opt-out)
最佳实践:仅在 .fullColor 渲染模式下显示全彩图像。策略性地使用 .widgetAccentable() 来创建视觉层次。使用多种强调色和背景图像进行测试。
容器背景
VStack { /* content */ }
.containerBackground(for: .widget) {
Color.blue.opacity(0.2)
}
在强调色模式下,系统会移除背景并将其替换为主题玻璃效果。若要防止背景被移除(这会使 Widget 无法显示在 iPad 锁定屏幕和待机模式中):
.containerBackgroundRemovable(false)
自定义 Widget 元素中的 Liquid Glass
Text("Label")
.padding()
.glassEffect() // Default capsule shape
Image(systemName: "star.fill")
.frame(width: 60, height: 60)
.glassEffect(.regular, in: .rect(cornerRadius: 12))
Button("Action") { }
.buttonStyle(.glass)
使用 GlassEffectContainer 组合多个玻璃效果元素:
GlassEffectContainer(spacing: 20.0) {
HStack(spacing: 20.0) {
Image(systemName: "cloud")
.frame(width: 60, height: 60)
.glassEffect()
Image(systemName: "sun")
.frame(width: 60, height: 60)
.glassEffect()
}
}
跨平台支持
visionOS Widget(visionOS 2+)
visionOS Widget 是放置在实体空间中的 3D 对象,可安装在表面上或悬浮显示。它们支持独特的空间功能。
安装样式
Widget 可以是凸起式(位于表面上方)或嵌入式(嵌入墙壁等垂直表面):
.supportedMountingStyles([.elevated, .recessed]) // Default is both
// .supportedMountingStyles([.recessed]) // Wall-only widget
如果仅限于 .recessed,用户就无法将 Widget 放置在水平表面上。
Widget 纹理
两种用于空间外观的视觉纹理:
.widgetTexture(.glass) // Default — transparent glass-like appearance
.widgetTexture(.paper) // Poster-like look, effective with extra-large sizes
距离感知(levelOfDetail)
Widget 会根据用户距离自动调整。系统会为不同细节级别之间的切换添加动画:
@Environment(\.levelOfDetail) var levelOfDetail
var body: some View {
VStack {
Text(entry.value)
.font(levelOfDetail == .simplified ? .largeTitle : .title)
}
}
取值:.default(近距离观看)和 .simplified(远距离观看——使用更大的文字和更少的细节)。
visionOS Widget 样式
visionOS 支持所有系统样式以及超大尺寸:
.supportedFamilies([
.systemSmall, .systemMedium, .systemLarge,
.systemExtraLarge,
.systemExtraLargePortrait // visionOS-specific portrait orientation
])
超大样式搭配 .widgetTexture(.paper) 尤其适合海报式展示。
背景检测
检测 Widget 背景是否可见(在强调色模式下会被移除):
@Environment(\.showsWidgetContainerBackground) var showsBackground
CarPlay(iOS 18+)
将 .supplementalActivityFamilies([.medium]) 添加到 ActivityConfiguration。使用待机模式风格的全宽仪表板呈现方式。
macOS 菜单栏
配对 iPhone 上的实时活动会自动显示在 macOS Sequoia+ 菜单栏中。无需更改代码。
watchOS 控件(11+)
ControlWidget 在 watchOS 上的工作方式相同,可用于控制中心、操作按钮和智能叠放。使用与 iOS 相同的 StaticControlConfiguration / ControlWidgetButton 模式。
相关性 Widget(iOS 18+)
使用 .relevanceConfiguration(for:score:attributes:) 帮助系统在智能叠放中优先显示 Widget。属性包括 .location(CLLocation)、.timeOfDay(DateInterval) 和 .activity(String),用于根据上下文进行排序。
推送通知更新(iOS 18+)
实现 PKPushRegistryDelegate 并处理 .widgetKit 推送类型,以接收从服务器推送到 Widget 的更新。更新共享容器中的数据,并调用 WidgetCenter.shared.reloadAllTimelines()。发送到 iPhone 的推送会自动同步到 Apple Watch 和 CarPlay。
第 8 部分:App Groups 与数据共享
App Groups 权限
在 App 与扩展之间共享数据时必需。
配置
- Xcode:Targets → Signing & Capabilities → Add "App Groups"
- 标识符格式:
group.com.company.appname - 为主应用目标和扩展目标都启用
共享容器
访问共享容器
let sharedContainer = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.mycompany.myapp"
)!
let dataFileURL = sharedContainer.appendingPathComponent("widgetData.json")
使用 App Groups 的 UserDefaults
// Main app - write data
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!
shared.set("Updated value", forKey: "myKey")
// Widget extension - read data
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!
let value = shared.string(forKey: "myKey")
使用 App Groups 的 Core Data
将 NSPersistentStoreDescription 指向共享容器 URL:
let sharedStoreURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.mycompany.myapp"
)!.appendingPathComponent("MyApp.sqlite")
let description = NSPersistentStoreDescription(url: sharedStoreURL)
container.persistentStoreDescriptions = [description]
IPC 通信
- 后台 URL Session — 将
config.sharedContainerIdentifier设置为你的 App Group ID,以便扩展访问下载内容 - Darwin 通知中心 — 使用
CFNotificationCenterPostNotification/CFNotificationCenterAddObserver和CFNotificationCenterGetDarwinNotifyCenter()发送简单的跨进程信号(例如,通知小组件调用WidgetCenter.shared.reloadAllTimelines())
第 9 部分:watchOS 集成
supplementalActivityFamilies(watchOS 11+)
将 .supplementalActivityFamilies([.small]) 添加到 ActivityConfiguration,即可在 Apple Watch Smart Stack 中显示 Live Activities(CarPlay 使用相同修饰符,值为 .medium)。
activityFamily 环境值
使用 @Environment(\.activityFamily) 调整布局 — 检查是 .small(watchOS)还是 iPhone 布局。
全天候显示屏
使用 @Environment(\.isLuminanceReduced) 简化全天候显示屏上的视图 — 减少细节、使用白色文字和更大的字体。结合 @Environment(\.colorScheme),以正确处理深色模式。
更新预算(watchOS)
watchOS 更新会通过推送通知自动与 iPhone 同步。如果手表超出蓝牙范围,更新可能会延迟。
第 10 部分:实用工作流
构建你的第一个小组件
如需包含可运行代码示例的完整分步教程,请参阅 Apple 的示例项目 使用 WidgetKit 和 SwiftUI 构建小组件。
关键步骤:添加小组件扩展目标、配置 App Groups、实现 TimelineProvider、设计 SwiftUI 视图,并从主应用更新。生产环境要求请参阅下方的专家审核清单。
专家审核清单
发布小组件前
架构:
- 在应用和扩展中配置 App Groups 权限
- 两个目标中的组标识符完全一致
- 所有数据共享都使用共享容器
- 小组件代码中不使用
UserDefaults.standard
性能:
- 时间线生成在 5 秒内完成
- 小组件视图中没有网络请求
- 时间线的刷新间隔合理(≥ 15 分钟)
- 条目数量合理(< 20-30 个)
- 内存用量低于限制(小组件约 30MB,实时活动约 50MB)
- 图像经过优化(优先使用资源目录或 SF Symbols)
数据与状态:
- 小组件能妥善处理缺失或 nil 数据
- 条目日期按时间顺序排列
- 占位视图外观合理
- 快照视图能代表实际使用情况
用户体验:
- 小组件显示在小组件图库中
- configurationDisplayName 清晰简洁
- description 说明小组件的用途
- 已测试所有支持的尺寸,显示效果正确
- 文本在浅色和深色背景下都清晰易读
- 交互元素(按钮/开关)正常工作
实时活动(如适用):
- ActivityAttributes 不超过 4KB
- 启动前检查授权状态
- 事件完成时结束活动
- 设置适当的关闭策略
- 如适用,配置 watchOS 支持(supplementalActivityFamilies)
- 已测试灵动岛布局(紧凑、最小化、展开)
Liquid Glass(如适用):
- 应用
widgetAccentable(),确保强调模式下的视觉层级 - 为图像设置
WidgetAccentedRenderingMode(.accented、.monochrome或.fullColor) - 已使用多种强调色和背景图像进行测试
- 使用
.containerBackground(for: .widget)配置容器背景
visionOS(如适用):
- 配置安装样式(
.elevated、.recessed或两者) - 选择小组件纹理(
.glass或.paper) - 为感知距离的布局处理
levelOfDetail - 视情况支持超大尺寸(
.systemExtraLarge、.systemExtraLargePortrait) - 已在不同距离下测试距离感知过渡效果
控制中心小组件(如适用):
- ControlValueProvider 异步且响应迅速(< 1 秒)
- previewValue 提供合理的备用值
- 已设置 displayName 和 description
- 已在控制中心、锁定屏幕和操作按钮中进行测试
测试:
- 已在真实设备上测试(而不仅是模拟器)
- 已测试添加和移除小组件
- 已测试应用数据变更后小组件是否更新
- 已测试强制退出应用后小组件是否仍能正常工作
- 已测试低内存场景
- 已测试所有支持的 iOS 版本
- 已测试无网络连接时的情况
测试指南
单元测试模式
测试 placeholder()、getSnapshot() 和 getTimeline() 方法。将测试数据保存到共享容器,使用模拟上下文调用 getTimeline(),并断言条目不为空且包含预期数据。异步生成时间线时,使用 waitForExpectations(timeout: 5.0)。
手动测试清单
- 将小组件添加到主屏幕,检查小组件图库、所有支持的尺寸,以及数据是否与应用一致
- 在主应用中更改数据,观察小组件是否更新;强制退出应用并重启设备
- 删除所有应用数据(检查能否妥善处理)、禁用网络(离线)、开启低电量模式、测试多个实例
- 在 Xcode Debug Navigator 中监控内存,在 Console 中检查时间线生成时间,并在较旧设备上测试
调试提示
- 在
getTimeline()中添加print()日志,确认该方法是否被调用以及数据是否已加载 - 验证 App Groups:分别在应用和小组件中输出
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier:);路径必须一致 - 在主应用中更改数据后,调用
WidgetCenter.shared.reloadAllTimelines()
第 11 部分:故障排查
小组件未显示在图库中:检查 WidgetBundle 是否包含该小组件,确认 supportedFamilies(),检查扩展的“Skip Install”是否设为 NO,并确认部署目标与应用一致。
小组件未刷新
症状:小组件显示的是过时数据,且不会更新
诊断步骤:
- 检查时间线策略(
.atEnd、.after()或.never) - 确认未超出每日预算(40-70 次重新加载)
- 检查是否调用了
getTimeline()(添加日志) - 确保已正确配置用于共享数据的 App Groups
解决方案:
// Manual reload from main app when data changes
import WidgetKit
WidgetCenter.shared.reloadAllTimelines()
// or
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")
应用与小组件之间的数据未共享
症状:小组件显示默认数据或空数据
诊断步骤:
- 确认两个目标都启用了 App Groups 权限
- 检查组标识符是否完全一致
- 确保两个目标使用相同的 suiteName
- 如果使用共享容器,请检查文件路径
解决方案:
// Both app AND extension must use:
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!
// NOT:
let shared = UserDefaults.standard // ❌ Different containers
Live Activity 无法启动
症状:Activity.request() 抛出错误
常见错误:
“Activity size exceeds 4KB”:
// ❌ BAD: Large images in attributes
struct MyAttributes: ActivityAttributes {
var productImage: UIImage // Too large!
}
// ✅ GOOD: Use asset catalog names
struct MyAttributes: ActivityAttributes {
var productImageName: String // Reference to asset
}
“Activities not enabled”:
// Check authorization first
let authInfo = ActivityAuthorizationInfo()
guard authInfo.areActivitiesEnabled else {
throw ActivityError.notEnabled
}
交互式小组件按钮不起作用
症状:点击按钮没有反应
诊断步骤:
- 确认 App Intent 的
perform()返回IntentResult - 检查小组件目标中是否导入了该 intent
- 确保按钮使用
intent:参数,而不是action: - 检查 Console 中是否有 intent 执行错误
解决方案:
// ✅ CORRECT: Use intent parameter
Button(intent: MyIntent()) {
Label("Action", systemImage: "star")
}
// ❌ WRONG: Don't use action closure
Button(action: { /* This won't work in widgets */ }) {
Label("Action", systemImage: "star")
}
控制中心小组件响应缓慢:在 ControlValueProvider.currentValue() 中使用异步操作,切勿使用 Thread.sleep 阻塞。提供快速的 previewValue 回退值。
小组件显示的尺寸不正确:在视图中根据 @Environment(\.widgetFamily) 进行切换,针对不同的 family 调整布局,避免使用硬编码尺寸。
时间线条目顺序错误:确保条目日期按时间顺序排列。使用从 Date() 开始递增的偏移量。
watchOS Live Activity 未显示:在 ActivityConfiguration 中添加 .supplementalActivityFamilies([.small]),确认使用 watchOS 11+,并检查蓝牙连接和配对状态。
性能问题
症状:小组件渲染缓慢、耗电量增加
常见原因:
- 时间线条目过多(> 100)
- 在视图代码中发起网络请求
- 在
getTimeline()中执行繁重计算 - 刷新间隔过短(< 15 min)
解决方案:
// ✅ GOOD: Strategic intervals
let entries = (0..<8).map { offset in
let date = Calendar.current.date(byAdding: .minute, value: offset * 15, to: now)!
return SimpleEntry(date: date, data: precomputedData)
}
// ❌ BAD: Too frequent, too many entries
let entries = (0..<100).map { offset in
let date = Calendar.current.date(byAdding: .minute, value: offset, to: now)!
return SimpleEntry(date: date, data: fetchFromNetwork()) // Network in timeline
}
调试小组件
模拟器与设备
- 模拟器:小组件会立即刷新,不受预算限制。适合测试布局,但无法准确反映刷新行为。
- 设备:受预算限制(每天 40-70 次重新加载)。发布前请在设备上测试,以确认实际刷新时机。
- Xcode 预览:可用于测试布局,但不会调用
getTimeline()。请通过单元测试或在设备上运行来测试时间线逻辑。
常见调试流程
- 在
getTimeline()中添加print(),确认其被调用且数据已加载 - 在 Console.app 中按小组件扩展进程名称筛选
- 使用
WidgetCenter.shared.getCurrentConfigurations()确认注册状态 - 如果应用更新后小组件仍显示旧数据,请确认 App Groups 容器路径一致
数据共享模式
小组件中的 SwiftData(iOS 17+):
- 在小组件中创建
ModelContainer,并使用与主应用相同的架构 - 使用共享的 App Groups 容器:
ModelConfiguration(url: containerURL) - 小组件只读取数据,不要从小组件写入,以避免冲突
- 主应用写入后调用
WidgetCenter.shared.reloadAllTimelines()
小组件中的 GRDB/SQLite:
- 通过 App Groups 容器共享数据库文件
- 并发读取时使用
DatabasePool(不要使用DatabaseQueue) - 小组件以只读方式打开连接:
try DatabasePool(path: dbPath, configuration: readOnlyConfig) - 在小组件中设置
configuration.readonly = true,以避免意外写入
资源
WWDC:2025-278、2024-10157、2024-10068、2024-10098、2023-10028、2023-10194、2022-10184、2022-10185
文档:/widgetkit、/activitykit、/appintents
技能:axiom-app-intents-ref、axiom-swift-concurrency、axiom-swiftui-performance、axiom-swiftui-layout、axiom-extensions-widgets
版本:0.9 | 平台:iOS 14+、iPadOS 14+、watchOS 9+、macOS 11+、visionOS 2+