@devo-bmad-custom/agent-orchestration 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/src/.agents/skills/tmux-commands/SKILL.md +1 -1
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/vision-framework/SKILL.md +475 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/vision-framework/references/vision-requests.md +736 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/vision-framework/references/visionkit-scanner.md +738 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/weatherkit/SKILL.md +410 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/weatherkit/references/weatherkit-patterns.md +567 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/widgetkit/SKILL.md +497 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/widgetkit/references/widgetkit-advanced.md +871 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/typography.csv +58 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/ui-reasoning.csv +101 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/ux-guidelines.csv +100 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/web-interface.csv +31 -0
- package/src/.agents/skills/ui-ux-pro-custom/scripts/core.py +253 -0
- package/src/.agents/skills/ui-ux-pro-custom/scripts/design_system.py +1067 -0
- package/src/.agents/skills/ui-ux-pro-custom/scripts/search.py +114 -0
- package/src/.agents/skills/ux-audit/SKILL.md +151 -0
- package/src/.agents/skills/websocket-engineer/SKILL.md +168 -0
- package/src/.agents/skills/websocket-engineer/references/alternatives.md +391 -0
- package/src/.agents/skills/websocket-engineer/references/patterns.md +400 -0
- package/src/.agents/skills/websocket-engineer/references/protocol.md +195 -0
- package/src/.agents/skills/websocket-engineer/references/scaling.md +333 -0
- package/src/.agents/skills/websocket-engineer/references/security.md +474 -0
- package/src/.agents/skills/writing-skills/SKILL.md +655 -0
- package/src/.agents/skills/writing-skills/anthropic-best-practices.md +1150 -0
- package/src/.agents/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/src/.agents/skills/writing-skills/graphviz-conventions.dot +172 -0
- package/src/.agents/skills/writing-skills/persuasion-principles.md +187 -0
- package/src/.agents/skills/writing-skills/render-graphs.js +168 -0
- package/src/.agents/skills/writing-skills/testing-skills-with-subagents.md +384 -0
- package/src/.claude/commands/bmad-master.md +15 -0
- package/src/.claude/commands/bmad-review-dry-loop.md +15 -0
- package/src/.claude/commands/bmad-review-dry.md +15 -0
- package/src/.claude/commands/bmad-review-security-loop.md +15 -0
- package/src/.claude/commands/bmad-review-security.md +15 -0
- package/src/.claude/commands/bmad-review-ui-loop.md +15 -0
- package/src/.claude/commands/bmad-review-ui.md +15 -0
- package/src/.claude/commands/bmad-track-compact.md +19 -0
- package/src/.claude/commands/bmad-track-extended.md +19 -0
- package/src/.claude/commands/bmad-track-large.md +19 -0
- package/src/.claude/commands/bmad-track-medium.md +19 -0
- package/src/.claude/commands/bmad-track-nano.md +19 -0
- package/src/.claude/commands/bmad-track-rv.md +18 -0
- package/src/.claude/commands/bmad-track-small.md +19 -0
- package/src/.claude/commands/bmad-triage.md +15 -0
- package/src/.claude/commands/master-orchestrator.md +15 -0
- package/src/_memory/master-orchestrator-sidecar/docs-index.md +3 -0
- package/src/_memory/master-orchestrator-sidecar/instructions.md +2616 -0
- package/src/_memory/master-orchestrator-sidecar/memories.md +8 -0
- package/src/_memory/master-orchestrator-sidecar/session-state.md +15 -0
- package/src/_memory/master-orchestrator-sidecar/triage-history.md +3 -0
- package/src/_memory/master-orchestrator-sidecar/workflows-overview.html +1230 -0
- package/src/core/agents/master-orchestrator.md +54 -0
- package/src/docs/dev/tmux/actions_popup.py +291 -0
- package/src/docs/dev/tmux/actions_popup.sh +110 -0
- package/src/docs/dev/tmux/claude_usage.sh +15 -0
- package/src/docs/dev/tmux/colors.conf +26 -0
- package/src/docs/dev/tmux/cpu_usage.sh +7 -0
- package/src/docs/dev/tmux/dispatch.sh +10 -0
- package/src/docs/dev/tmux/float_init.sh +13 -0
- package/src/docs/dev/tmux/float_term.sh +23 -0
- package/src/docs/dev/tmux/open_clip.sh +14 -0
- package/src/docs/dev/tmux/paste_claude.sh +26 -0
- package/src/docs/dev/tmux/paste_clipboard.sh +13 -0
- package/src/docs/dev/tmux/paste_image_wrapper.sh +98 -0
- package/src/docs/dev/tmux/ram_usage.sh +3 -0
- package/src/docs/dev/tmux/title_sync.sh +54 -0
- package/src/docs/dev/tmux/tmux-setup.md +867 -0
- package/src/docs/dev/tmux/tmux-test.sh +255 -0
- package/src/docs/dev/tmux/tmux.conf +127 -0
- package/src/docs/dev/tmux/xclip +18 -0
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
# WidgetKit Advanced Reference
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- [Timeline Strategies](#timeline-strategies)
|
|
6
|
+
- [Push-Based Timeline Reloads (iOS 26+)](#push-based-timeline-reloads-ios-26)
|
|
7
|
+
- [Widget URL Handling and Deep Links](#widget-url-handling-and-deep-links)
|
|
8
|
+
- [Intent-Driven Widget Configuration](#intent-driven-widget-configuration)
|
|
9
|
+
- [Multiple Widget Support in WidgetBundle](#multiple-widget-support-in-widgetbundle)
|
|
10
|
+
- [Widget Previews and Snapshots](#widget-previews-and-snapshots)
|
|
11
|
+
- [AccessoryWidgetBackground](#accessorywidgetbackground)
|
|
12
|
+
- [Dynamic Island Expanded Layout Patterns](#dynamic-island-expanded-layout-patterns)
|
|
13
|
+
- [Alert Configuration for Live Activities](#alert-configuration-for-live-activities)
|
|
14
|
+
- [Push Notification Support for Live Activities](#push-notification-support-for-live-activities)
|
|
15
|
+
- [ActivityAuthorizationInfo](#activityauthorizationinfo)
|
|
16
|
+
- [Widget Performance Best Practices](#widget-performance-best-practices)
|
|
17
|
+
- [Xcode Setup](#xcode-setup)
|
|
18
|
+
- [Widget Relevance and Smart Stacks](#widget-relevance-and-smart-stacks)
|
|
19
|
+
- [ActivityState Lifecycle](#activitystate-lifecycle)
|
|
20
|
+
- [ActivityStyle](#activitystyle)
|
|
21
|
+
- [Dismissal Policies](#dismissal-policies)
|
|
22
|
+
- [Querying Active Widgets and Activities](#querying-active-widgets-and-activities)
|
|
23
|
+
- [Apple Documentation Links](#apple-documentation-links)
|
|
24
|
+
|
|
25
|
+
## Timeline Strategies
|
|
26
|
+
|
|
27
|
+
### TimelineReloadPolicy
|
|
28
|
+
|
|
29
|
+
Control when WidgetKit requests a new timeline after the current entries expire.
|
|
30
|
+
|
|
31
|
+
| Policy | Behavior | Use When |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `.atEnd` | Requests a new timeline after the last entry's date. Default. | Data changes unpredictably. |
|
|
34
|
+
| `.after(Date)` | Requests a new timeline after a specific date. | Data updates on a known schedule (market hours, flights). |
|
|
35
|
+
| `.never` | No automatic refresh. App must trigger manually. | Data changes only from user action. |
|
|
36
|
+
|
|
37
|
+
### Multiple Timeline Entries
|
|
38
|
+
|
|
39
|
+
Pre-generate entries for known future states to reduce refresh requests and
|
|
40
|
+
conserve the daily budget.
|
|
41
|
+
|
|
42
|
+
```swift
|
|
43
|
+
func timeline(for configuration: Intent, in context: Context) async -> Timeline<StockEntry> {
|
|
44
|
+
var entries: [StockEntry] = []
|
|
45
|
+
let now = Date()
|
|
46
|
+
|
|
47
|
+
// Generate hourly entries for the next 6 hours
|
|
48
|
+
for hourOffset in 0..<6 {
|
|
49
|
+
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: now)!
|
|
50
|
+
let price = await StockService.shared.projectedPrice(at: entryDate, for: configuration.symbol)
|
|
51
|
+
entries.append(StockEntry(date: entryDate, symbol: configuration.symbol.name, price: price))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let nextRefresh = Calendar.current.date(byAdding: .hour, value: 6, to: now)!
|
|
55
|
+
return Timeline(entries: entries, policy: .after(nextRefresh))
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Triggering Manual Reloads
|
|
60
|
+
|
|
61
|
+
```swift
|
|
62
|
+
// Reload a specific widget kind
|
|
63
|
+
WidgetCenter.shared.reloadTimelines(ofKind: "OrderStatusWidget")
|
|
64
|
+
|
|
65
|
+
// Reload all widgets
|
|
66
|
+
WidgetCenter.shared.reloadAllTimelines()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Call `reloadTimelines(ofKind:)` only when displayed data actually changes. Each
|
|
70
|
+
call counts against the daily refresh budget.
|
|
71
|
+
|
|
72
|
+
### Refresh Budget
|
|
73
|
+
|
|
74
|
+
Each configured widget has a daily refresh limit. Exemptions apply for:
|
|
75
|
+
- Foreground app usage
|
|
76
|
+
- Active media sessions
|
|
77
|
+
- Standard location service usage
|
|
78
|
+
|
|
79
|
+
WidgetKit does not impose refresh limits when debugging in Xcode.
|
|
80
|
+
|
|
81
|
+
## Push-Based Timeline Reloads (iOS 26+)
|
|
82
|
+
|
|
83
|
+
### WidgetPushHandler
|
|
84
|
+
|
|
85
|
+
Use push notifications to trigger timeline reloads without scheduled polling.
|
|
86
|
+
|
|
87
|
+
```swift
|
|
88
|
+
struct MyWidgetPushHandler: WidgetPushHandler {
|
|
89
|
+
func pushTokenDidChange(_ pushInfo: WidgetPushInfo, widgets: [WidgetInfo]) {
|
|
90
|
+
let tokenString = pushInfo.token.map { String(format: "%02x", $0) }.joined()
|
|
91
|
+
Task {
|
|
92
|
+
try await ServerAPI.shared.register(widgetPushToken: tokenString)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Server-Side Integration
|
|
99
|
+
|
|
100
|
+
Send an APNs push with the widget's push token. The system calls your
|
|
101
|
+
`TimelineProvider.getTimeline` or `AppIntentTimelineProvider.timeline(for:in:)`
|
|
102
|
+
when the push arrives.
|
|
103
|
+
|
|
104
|
+
### ControlPushHandler
|
|
105
|
+
|
|
106
|
+
Equivalent handler for Control Center controls:
|
|
107
|
+
|
|
108
|
+
```swift
|
|
109
|
+
struct MyControlPushHandler: ControlPushHandler {
|
|
110
|
+
func pushTokensDidChange(controls: [ControlPushInfo]) {
|
|
111
|
+
for control in controls {
|
|
112
|
+
let tokenString = control.token.map { String(format: "%02x", $0) }.joined()
|
|
113
|
+
Task {
|
|
114
|
+
try await ServerAPI.shared.register(controlPushToken: tokenString)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Widget URL Handling and Deep Links
|
|
122
|
+
|
|
123
|
+
### widgetURL(_:)
|
|
124
|
+
|
|
125
|
+
Set a single URL for the entire widget. Tapping anywhere opens the app with this URL.
|
|
126
|
+
|
|
127
|
+
```swift
|
|
128
|
+
struct SmallWidgetView: View {
|
|
129
|
+
let entry: OrderEntry
|
|
130
|
+
|
|
131
|
+
var body: some View {
|
|
132
|
+
VStack {
|
|
133
|
+
Text(entry.orderName)
|
|
134
|
+
Text(entry.status)
|
|
135
|
+
}
|
|
136
|
+
.widgetURL(URL(string: "myapp://orders/\(entry.orderID)")!)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Link (Medium and Larger Widgets)
|
|
142
|
+
|
|
143
|
+
Use `Link` for multiple tap targets in `.systemMedium` and larger widgets.
|
|
144
|
+
|
|
145
|
+
```swift
|
|
146
|
+
struct MediumWidgetView: View {
|
|
147
|
+
let entry: OrderListEntry
|
|
148
|
+
|
|
149
|
+
var body: some View {
|
|
150
|
+
VStack {
|
|
151
|
+
ForEach(entry.orders) { order in
|
|
152
|
+
Link(destination: URL(string: "myapp://orders/\(order.id)")!) {
|
|
153
|
+
HStack {
|
|
154
|
+
Text(order.name)
|
|
155
|
+
Spacer()
|
|
156
|
+
Text(order.status)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Handling in the App
|
|
166
|
+
|
|
167
|
+
```swift
|
|
168
|
+
@main
|
|
169
|
+
struct MyApp: App {
|
|
170
|
+
var body: some Scene {
|
|
171
|
+
WindowGroup {
|
|
172
|
+
ContentView()
|
|
173
|
+
.onOpenURL { url in
|
|
174
|
+
DeepLinkRouter.shared.handle(url)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Important:** `.systemSmall` widgets support only `widgetURL`, not `Link`.
|
|
182
|
+
|
|
183
|
+
## Intent-Driven Widget Configuration
|
|
184
|
+
|
|
185
|
+
### Defining a WidgetConfigurationIntent
|
|
186
|
+
|
|
187
|
+
```swift
|
|
188
|
+
struct SelectCategoryIntent: WidgetConfigurationIntent {
|
|
189
|
+
static var title: LocalizedStringResource = "Select Category"
|
|
190
|
+
static var description: IntentDescription = "Choose a category to display."
|
|
191
|
+
|
|
192
|
+
@Parameter(title: "Category")
|
|
193
|
+
var category: CategoryEntity
|
|
194
|
+
|
|
195
|
+
init() {}
|
|
196
|
+
|
|
197
|
+
init(category: CategoryEntity) {
|
|
198
|
+
self.category = category
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Entity Query for Dynamic Options
|
|
204
|
+
|
|
205
|
+
```swift
|
|
206
|
+
struct CategoryEntity: AppEntity {
|
|
207
|
+
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Category")
|
|
208
|
+
static var defaultQuery = CategoryQuery()
|
|
209
|
+
|
|
210
|
+
var id: String
|
|
211
|
+
var name: String
|
|
212
|
+
|
|
213
|
+
var displayRepresentation: DisplayRepresentation {
|
|
214
|
+
DisplayRepresentation(title: "\(name)")
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
struct CategoryQuery: EntityQuery {
|
|
219
|
+
func entities(for identifiers: [String]) async throws -> [CategoryEntity] {
|
|
220
|
+
await DataStore.shared.categories(for: identifiers)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
func suggestedEntities() async throws -> [CategoryEntity] {
|
|
224
|
+
await DataStore.shared.allCategories()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
func defaultResult() async -> CategoryEntity? {
|
|
228
|
+
await DataStore.shared.defaultCategory()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Recommendations
|
|
234
|
+
|
|
235
|
+
Provide pre-configured suggestions for the widget gallery:
|
|
236
|
+
|
|
237
|
+
```swift
|
|
238
|
+
func recommendations() -> [AppIntentRecommendation<SelectCategoryIntent>] {
|
|
239
|
+
let categories: [(String, CategoryEntity)] = [
|
|
240
|
+
("Groceries", .groceries),
|
|
241
|
+
("Work Tasks", .work),
|
|
242
|
+
]
|
|
243
|
+
return categories.map { name, entity in
|
|
244
|
+
let intent = SelectCategoryIntent(category: entity)
|
|
245
|
+
return AppIntentRecommendation(intent: intent, description: name)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Multiple Widget Support in WidgetBundle
|
|
251
|
+
|
|
252
|
+
### Declaring Multiple Widgets
|
|
253
|
+
|
|
254
|
+
```swift
|
|
255
|
+
@main
|
|
256
|
+
struct MyAppWidgets: WidgetBundle {
|
|
257
|
+
var body: some Widget {
|
|
258
|
+
OrderStatusWidget() // Home Screen widget
|
|
259
|
+
FavoritesWidget() // Configurable widget
|
|
260
|
+
StepsAccessoryWidget() // Lock Screen widget
|
|
261
|
+
DeliveryActivityWidget() // Live Activity
|
|
262
|
+
QuickActionControl() // Control Center
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Conditional Widgets
|
|
268
|
+
|
|
269
|
+
Include widgets conditionally based on platform or availability:
|
|
270
|
+
|
|
271
|
+
```swift
|
|
272
|
+
@main
|
|
273
|
+
struct MyAppWidgets: WidgetBundle {
|
|
274
|
+
var body: some Widget {
|
|
275
|
+
CoreWidget()
|
|
276
|
+
if #available(iOS 18, *) {
|
|
277
|
+
QuickActionControl()
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Widget Previews and Snapshots
|
|
284
|
+
|
|
285
|
+
### Xcode Previews
|
|
286
|
+
|
|
287
|
+
```swift
|
|
288
|
+
#Preview("Small", as: .systemSmall) {
|
|
289
|
+
OrderStatusWidget()
|
|
290
|
+
} timeline: {
|
|
291
|
+
OrderEntry(date: .now, orderName: "Pizza", status: "Preparing")
|
|
292
|
+
OrderEntry(date: .now.addingTimeInterval(600), orderName: "Pizza", status: "Delivering")
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#Preview("Circular", as: .accessoryCircular) {
|
|
296
|
+
StepsAccessoryWidget()
|
|
297
|
+
} timeline: {
|
|
298
|
+
StepsEntry(date: .now, stepCount: 4200)
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Live Activity Previews
|
|
303
|
+
|
|
304
|
+
```swift
|
|
305
|
+
#Preview("Lock Screen", as: .content, using: DeliveryAttributes.preview) {
|
|
306
|
+
DeliveryActivityWidget()
|
|
307
|
+
} contentStates: {
|
|
308
|
+
DeliveryAttributes.ContentState(
|
|
309
|
+
driverName: "Alex",
|
|
310
|
+
estimatedDeliveryTime: Date()...Date().addingTimeInterval(900),
|
|
311
|
+
currentStep: .delivering
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#Preview("Dynamic Island Compact", as: .dynamicIsland(.compact), using: DeliveryAttributes.preview) {
|
|
316
|
+
DeliveryActivityWidget()
|
|
317
|
+
} contentStates: {
|
|
318
|
+
DeliveryAttributes.ContentState(
|
|
319
|
+
driverName: "Alex",
|
|
320
|
+
estimatedDeliveryTime: Date()...Date().addingTimeInterval(900),
|
|
321
|
+
currentStep: .delivering
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Snapshot Best Practices
|
|
327
|
+
|
|
328
|
+
- Return sample data immediately in `placeholder(in:)` -- it must be synchronous.
|
|
329
|
+
- In `getSnapshot` / `snapshot(for:in:)`, check `context.isPreview`:
|
|
330
|
+
- When `true`, return representative sample data quickly.
|
|
331
|
+
- When `false`, return the current real state.
|
|
332
|
+
|
|
333
|
+
```swift
|
|
334
|
+
// WRONG: Performing a network call in placeholder
|
|
335
|
+
func placeholder(in context: Context) -> MyEntry {
|
|
336
|
+
// Compilation error: placeholder must be synchronous
|
|
337
|
+
let data = await fetchData()
|
|
338
|
+
return MyEntry(date: .now, data: data)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// CORRECT: Return static sample data
|
|
342
|
+
func placeholder(in context: Context) -> MyEntry {
|
|
343
|
+
MyEntry(date: .now, data: SampleData.placeholder)
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## AccessoryWidgetBackground
|
|
348
|
+
|
|
349
|
+
Provide the standard translucent background for Lock Screen widgets.
|
|
350
|
+
|
|
351
|
+
```swift
|
|
352
|
+
struct CircularStepsView: View {
|
|
353
|
+
let steps: Int
|
|
354
|
+
|
|
355
|
+
var body: some View {
|
|
356
|
+
ZStack {
|
|
357
|
+
AccessoryWidgetBackground()
|
|
358
|
+
VStack(spacing: 2) {
|
|
359
|
+
Image(systemName: "figure.walk")
|
|
360
|
+
.font(.caption)
|
|
361
|
+
Text("\(steps)")
|
|
362
|
+
.font(.headline)
|
|
363
|
+
.widgetAccentable()
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Rendering Mode Awareness
|
|
371
|
+
|
|
372
|
+
Lock Screen widgets render in `.vibrant` or `.accented` mode. Adapt content:
|
|
373
|
+
|
|
374
|
+
```swift
|
|
375
|
+
@Environment(\.widgetRenderingMode) var renderingMode
|
|
376
|
+
|
|
377
|
+
var body: some View {
|
|
378
|
+
switch renderingMode {
|
|
379
|
+
case .fullColor:
|
|
380
|
+
ColorfulView()
|
|
381
|
+
case .vibrant, .accented:
|
|
382
|
+
MonochromeView()
|
|
383
|
+
@unknown default:
|
|
384
|
+
MonochromeView()
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Use `.widgetAccentable()` to mark views that should receive the accent tint in
|
|
390
|
+
`.accented` rendering mode.
|
|
391
|
+
|
|
392
|
+
## Dynamic Island Expanded Layout Patterns
|
|
393
|
+
|
|
394
|
+
### Full Layout Example
|
|
395
|
+
|
|
396
|
+
```swift
|
|
397
|
+
DynamicIsland {
|
|
398
|
+
DynamicIslandExpandedRegion(.leading) {
|
|
399
|
+
VStack(alignment: .leading) {
|
|
400
|
+
Image(systemName: "airplane")
|
|
401
|
+
.font(.title2)
|
|
402
|
+
Text("UA 1234")
|
|
403
|
+
.font(.caption2)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
DynamicIslandExpandedRegion(.trailing) {
|
|
407
|
+
VStack(alignment: .trailing) {
|
|
408
|
+
Text("SFO")
|
|
409
|
+
.font(.title3.bold())
|
|
410
|
+
Text("On Time")
|
|
411
|
+
.font(.caption2)
|
|
412
|
+
.foregroundStyle(.green)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
DynamicIslandExpandedRegion(.center) {
|
|
416
|
+
Text("San Francisco to New York")
|
|
417
|
+
.font(.caption)
|
|
418
|
+
.lineLimit(1)
|
|
419
|
+
}
|
|
420
|
+
DynamicIslandExpandedRegion(.bottom) {
|
|
421
|
+
ProgressView(value: 0.45)
|
|
422
|
+
.tint(.blue)
|
|
423
|
+
HStack {
|
|
424
|
+
Text("Departed 2:30 PM")
|
|
425
|
+
Spacer()
|
|
426
|
+
Text("Arrives 10:45 PM")
|
|
427
|
+
}
|
|
428
|
+
.font(.caption2)
|
|
429
|
+
.foregroundStyle(.secondary)
|
|
430
|
+
}
|
|
431
|
+
} compactLeading: {
|
|
432
|
+
Image(systemName: "airplane")
|
|
433
|
+
} compactTrailing: {
|
|
434
|
+
Text("2h 15m")
|
|
435
|
+
.monospacedDigit()
|
|
436
|
+
} minimal: {
|
|
437
|
+
Image(systemName: "airplane")
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Vertical Placement
|
|
442
|
+
|
|
443
|
+
Control vertical alignment within expanded regions:
|
|
444
|
+
|
|
445
|
+
```swift
|
|
446
|
+
DynamicIslandExpandedRegion(.leading) {
|
|
447
|
+
Text("Top")
|
|
448
|
+
.dynamicIsland(verticalPlacement: .belowIfTooWide)
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Content Margins
|
|
453
|
+
|
|
454
|
+
Override margins for specific Dynamic Island modes:
|
|
455
|
+
|
|
456
|
+
```swift
|
|
457
|
+
.contentMargins(.trailing, 20, for: .expanded)
|
|
458
|
+
.contentMargins(.bottom, 16, for: .expanded)
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Keyline Tint
|
|
462
|
+
|
|
463
|
+
Apply a subtle tint to the Dynamic Island border:
|
|
464
|
+
|
|
465
|
+
```swift
|
|
466
|
+
DynamicIsland { /* ... */ }
|
|
467
|
+
.keylineTint(.blue)
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Alert Configuration for Live Activities
|
|
471
|
+
|
|
472
|
+
Trigger a visible and audible alert when updating a Live Activity:
|
|
473
|
+
|
|
474
|
+
```swift
|
|
475
|
+
let alert = AlertConfiguration(
|
|
476
|
+
title: "Delivery Update",
|
|
477
|
+
body: "Your order is out for delivery!",
|
|
478
|
+
sound: .default
|
|
479
|
+
)
|
|
480
|
+
await activity.update(updatedContent, alertConfiguration: alert)
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Custom Alert Sound
|
|
484
|
+
|
|
485
|
+
```swift
|
|
486
|
+
let alert = AlertConfiguration(
|
|
487
|
+
title: "Score Update",
|
|
488
|
+
body: "Goal! The score is now 2-1.",
|
|
489
|
+
sound: .named("goal-horn.aiff")
|
|
490
|
+
)
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Place the sound file in the app bundle. Use `.default` when no custom sound is needed.
|
|
494
|
+
|
|
495
|
+
## Push Notification Support for Live Activities
|
|
496
|
+
|
|
497
|
+
### Registering for Push Updates
|
|
498
|
+
|
|
499
|
+
```swift
|
|
500
|
+
let activity = try Activity.request(
|
|
501
|
+
attributes: attributes,
|
|
502
|
+
content: content,
|
|
503
|
+
pushType: .token // Enable push updates
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
// Observe token changes
|
|
507
|
+
Task {
|
|
508
|
+
for await token in activity.pushTokenUpdates {
|
|
509
|
+
let tokenString = token.map { String(format: "%02x", $0) }.joined()
|
|
510
|
+
try await ServerAPI.shared.registerActivityToken(tokenString, activityID: activity.id)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Push-to-Start (Remote Activity Creation)
|
|
516
|
+
|
|
517
|
+
```swift
|
|
518
|
+
// Observe the push-to-start token
|
|
519
|
+
Task {
|
|
520
|
+
for await token in Activity<DeliveryAttributes>.pushToStartTokenUpdates {
|
|
521
|
+
let tokenString = token.map { String(format: "%02x", $0) }.joined()
|
|
522
|
+
try await ServerAPI.shared.registerPushToStartToken(tokenString)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Channel-Based Push (iOS 26+)
|
|
528
|
+
|
|
529
|
+
```swift
|
|
530
|
+
let activity = try Activity.request(
|
|
531
|
+
attributes: attributes,
|
|
532
|
+
content: content,
|
|
533
|
+
pushType: .channel("delivery-updates")
|
|
534
|
+
)
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### APNs Payload Format for Live Activity Updates
|
|
538
|
+
|
|
539
|
+
```json
|
|
540
|
+
{
|
|
541
|
+
"aps": {
|
|
542
|
+
"timestamp": 1234567890,
|
|
543
|
+
"event": "update",
|
|
544
|
+
"content-state": {
|
|
545
|
+
"driverName": "Alex",
|
|
546
|
+
"estimatedDeliveryTime": {
|
|
547
|
+
"lowerBound": 1234567890,
|
|
548
|
+
"upperBound": 1234568790
|
|
549
|
+
},
|
|
550
|
+
"currentStep": "delivering"
|
|
551
|
+
},
|
|
552
|
+
"alert": {
|
|
553
|
+
"title": "Delivery Update",
|
|
554
|
+
"body": "Your driver is nearby!"
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
The `content-state` must match the `ContentState` Codable structure exactly.
|
|
561
|
+
|
|
562
|
+
### Info.plist Keys
|
|
563
|
+
|
|
564
|
+
| Key | Value | Purpose |
|
|
565
|
+
|---|---|---|
|
|
566
|
+
| `NSSupportsLiveActivities` | `YES` | Enable Live Activities |
|
|
567
|
+
| `NSSupportsLiveActivitiesFrequentUpdates` | `YES` | Enable frequent push updates (budget increase) |
|
|
568
|
+
|
|
569
|
+
## ActivityAuthorizationInfo
|
|
570
|
+
|
|
571
|
+
Check whether Live Activities are permitted before attempting to start one.
|
|
572
|
+
|
|
573
|
+
```swift
|
|
574
|
+
let authInfo = ActivityAuthorizationInfo()
|
|
575
|
+
|
|
576
|
+
// Check permission synchronously
|
|
577
|
+
if authInfo.areActivitiesEnabled {
|
|
578
|
+
try Activity.request(attributes: attributes, content: content, pushType: .token)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Observe permission changes
|
|
582
|
+
Task {
|
|
583
|
+
for await enabled in authInfo.activityEnablementUpdates {
|
|
584
|
+
if enabled {
|
|
585
|
+
// Activities became available
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Check frequent push support
|
|
591
|
+
if authInfo.frequentPushesEnabled {
|
|
592
|
+
// Safe to use frequent push updates
|
|
593
|
+
}
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### Error Handling
|
|
597
|
+
|
|
598
|
+
```swift
|
|
599
|
+
do {
|
|
600
|
+
let activity = try Activity.request(attributes: attributes, content: content, pushType: .token)
|
|
601
|
+
} catch let error as ActivityAuthorizationError {
|
|
602
|
+
switch error {
|
|
603
|
+
case .denied:
|
|
604
|
+
// User disabled Live Activities in Settings
|
|
605
|
+
break
|
|
606
|
+
case .globalMaximumExceeded:
|
|
607
|
+
// Too many Live Activities across all apps
|
|
608
|
+
break
|
|
609
|
+
case .targetMaximumExceeded:
|
|
610
|
+
// Too many Live Activities for this app
|
|
611
|
+
break
|
|
612
|
+
default:
|
|
613
|
+
break
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
## Widget Performance Best Practices
|
|
619
|
+
|
|
620
|
+
### Data Preparation
|
|
621
|
+
|
|
622
|
+
Pre-compute display values in the timeline provider. Pass display-ready data
|
|
623
|
+
through the entry.
|
|
624
|
+
|
|
625
|
+
```swift
|
|
626
|
+
// WRONG: Heavy computation in the widget view
|
|
627
|
+
struct MyWidgetView: View {
|
|
628
|
+
let entry: RawDataEntry
|
|
629
|
+
|
|
630
|
+
var body: some View {
|
|
631
|
+
let processed = HeavyProcessor.process(entry.rawData) // Slow
|
|
632
|
+
Text(processed.summary)
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// CORRECT: Pre-compute in the provider
|
|
637
|
+
func timeline(for configuration: Intent, in context: Context) async -> Timeline<ProcessedEntry> {
|
|
638
|
+
let raw = await DataStore.shared.fetch()
|
|
639
|
+
let processed = HeavyProcessor.process(raw)
|
|
640
|
+
let entry = ProcessedEntry(date: .now, summary: processed.summary, value: processed.value)
|
|
641
|
+
return Timeline(entries: [entry], policy: .atEnd)
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Memory Constraints
|
|
646
|
+
|
|
647
|
+
Widget extensions run with strict memory limits. Avoid:
|
|
648
|
+
- Loading large images directly in the widget view
|
|
649
|
+
- Storing large data sets in the entry
|
|
650
|
+
- Creating complex view hierarchies
|
|
651
|
+
|
|
652
|
+
### Image Handling
|
|
653
|
+
|
|
654
|
+
```swift
|
|
655
|
+
// WRONG: Loading a full-resolution image
|
|
656
|
+
Image(uiImage: UIImage(contentsOfFile: fullResPath)!)
|
|
657
|
+
|
|
658
|
+
// CORRECT: Use a pre-resized thumbnail stored in the shared container
|
|
659
|
+
Image(uiImage: UIImage(contentsOfFile: thumbnailPath)!)
|
|
660
|
+
.resizable()
|
|
661
|
+
.aspectRatio(contentMode: .fill)
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### Shared Data with App Groups
|
|
665
|
+
|
|
666
|
+
```swift
|
|
667
|
+
// In the main app: write data
|
|
668
|
+
let defaults = UserDefaults(suiteName: "group.com.example.myapp")
|
|
669
|
+
defaults?.set(encodedData, forKey: "widgetData")
|
|
670
|
+
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")
|
|
671
|
+
|
|
672
|
+
// In the widget provider: read data
|
|
673
|
+
func timeline(for configuration: Intent, in context: Context) async -> Timeline<MyEntry> {
|
|
674
|
+
let defaults = UserDefaults(suiteName: "group.com.example.myapp")
|
|
675
|
+
let data = defaults?.data(forKey: "widgetData")
|
|
676
|
+
// Decode and build entry
|
|
677
|
+
}
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
For larger datasets, use a shared SQLite database or Core Data store in the
|
|
681
|
+
App Group container.
|
|
682
|
+
|
|
683
|
+
## Xcode Setup
|
|
684
|
+
|
|
685
|
+
### Adding a Widget Extension Target
|
|
686
|
+
|
|
687
|
+
1. File > New > Target > Widget Extension.
|
|
688
|
+
2. Name the extension (e.g., "MyAppWidgets").
|
|
689
|
+
3. Select "Include Configuration App Intent" for configurable widgets.
|
|
690
|
+
4. Select "Include Live Activity" if building Live Activities.
|
|
691
|
+
|
|
692
|
+
### Entitlements
|
|
693
|
+
|
|
694
|
+
| Entitlement | Purpose |
|
|
695
|
+
|---|---|
|
|
696
|
+
| App Groups (`com.apple.security.application-groups`) | Share data between app and widget |
|
|
697
|
+
| Push Notifications (`aps-environment`) | Required for push-based Live Activity updates |
|
|
698
|
+
|
|
699
|
+
### App Groups Configuration
|
|
700
|
+
|
|
701
|
+
1. Enable "App Groups" capability on both the main app target and the widget
|
|
702
|
+
extension target.
|
|
703
|
+
2. Create a shared group identifier (e.g., `group.com.example.myapp`).
|
|
704
|
+
3. Use `UserDefaults(suiteName:)` or `FileManager.containerURL(forSecurityApplicationGroupIdentifier:)`
|
|
705
|
+
for shared storage.
|
|
706
|
+
|
|
707
|
+
### Build Schemes
|
|
708
|
+
|
|
709
|
+
- Use the widget extension scheme to debug widget rendering.
|
|
710
|
+
- Select "Widget" as the run destination to launch the widget directly.
|
|
711
|
+
- Use "Preview" in Xcode canvas for rapid iteration.
|
|
712
|
+
|
|
713
|
+
### Common Xcode Issues
|
|
714
|
+
|
|
715
|
+
```text
|
|
716
|
+
// ERROR: "Widget extension must include at least one widget"
|
|
717
|
+
// FIX: Ensure @main is on the WidgetBundle, not a widget struct.
|
|
718
|
+
|
|
719
|
+
// ERROR: "No such module 'WidgetKit'"
|
|
720
|
+
// FIX: Ensure the widget extension target links WidgetKit and SwiftUI frameworks.
|
|
721
|
+
|
|
722
|
+
// ERROR: "The operation couldn't be completed. (ActivityKit.ActivityAuthorizationError error 3.)"
|
|
723
|
+
// FIX: Add NSSupportsLiveActivities = YES to the HOST APP's Info.plist (not the extension).
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
## Widget Relevance and Smart Stacks
|
|
727
|
+
|
|
728
|
+
### TimelineEntryRelevance
|
|
729
|
+
|
|
730
|
+
Score entries to surface widgets in Smart Stacks when relevant:
|
|
731
|
+
|
|
732
|
+
```swift
|
|
733
|
+
struct GameEntry: TimelineEntry {
|
|
734
|
+
var date: Date
|
|
735
|
+
var score: String
|
|
736
|
+
var isLive: Bool
|
|
737
|
+
|
|
738
|
+
var relevance: TimelineEntryRelevance? {
|
|
739
|
+
isLive ? TimelineEntryRelevance(score: 100, duration: 3600) : nil
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
Higher scores make the widget more likely to surface. The `duration` specifies
|
|
745
|
+
how long the relevance lasts.
|
|
746
|
+
|
|
747
|
+
### WidgetRelevance (AppIntentTimelineProvider)
|
|
748
|
+
|
|
749
|
+
```swift
|
|
750
|
+
func relevance() async -> WidgetRelevance<SelectCategoryIntent> {
|
|
751
|
+
let topCategory = await DataStore.shared.mostActiveCategory()
|
|
752
|
+
let intent = SelectCategoryIntent(category: topCategory)
|
|
753
|
+
return WidgetRelevance(intent, score: 80)
|
|
754
|
+
}
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
## ActivityState Lifecycle
|
|
758
|
+
|
|
759
|
+
Track the full lifecycle of a Live Activity:
|
|
760
|
+
|
|
761
|
+
```swift
|
|
762
|
+
Task {
|
|
763
|
+
for await state in activity.activityStateUpdates {
|
|
764
|
+
switch state {
|
|
765
|
+
case .active:
|
|
766
|
+
// Activity is running and visible
|
|
767
|
+
break
|
|
768
|
+
case .pending:
|
|
769
|
+
// Requested but not yet displayed (iOS 26+)
|
|
770
|
+
break
|
|
771
|
+
case .stale:
|
|
772
|
+
// Content is outdated; update or end
|
|
773
|
+
break
|
|
774
|
+
case .ended:
|
|
775
|
+
// Ended but may still be visible on Lock Screen
|
|
776
|
+
break
|
|
777
|
+
case .dismissed:
|
|
778
|
+
// Fully removed from UI; clean up resources
|
|
779
|
+
break
|
|
780
|
+
@unknown default:
|
|
781
|
+
break
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
## ActivityStyle
|
|
788
|
+
|
|
789
|
+
Control Live Activity persistence behavior (iOS 18+):
|
|
790
|
+
|
|
791
|
+
```swift
|
|
792
|
+
// Standard: persists until explicitly ended
|
|
793
|
+
let activity = try Activity.request(
|
|
794
|
+
attributes: attributes,
|
|
795
|
+
content: content,
|
|
796
|
+
pushType: .token,
|
|
797
|
+
style: .standard
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
// Transient: automatically dismissed after a period
|
|
801
|
+
let activity = try Activity.request(
|
|
802
|
+
attributes: attributes,
|
|
803
|
+
content: content,
|
|
804
|
+
pushType: .token,
|
|
805
|
+
style: .transient
|
|
806
|
+
)
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
Use `.transient` for short-lived notifications like sports scores or transit
|
|
810
|
+
arrivals that do not need persistent display.
|
|
811
|
+
|
|
812
|
+
## Dismissal Policies
|
|
813
|
+
|
|
814
|
+
Control when an ended Live Activity disappears from the Lock Screen:
|
|
815
|
+
|
|
816
|
+
```swift
|
|
817
|
+
// System-determined timing (default)
|
|
818
|
+
await activity.end(finalContent, dismissalPolicy: .default)
|
|
819
|
+
|
|
820
|
+
// Remove immediately
|
|
821
|
+
await activity.end(finalContent, dismissalPolicy: .immediate)
|
|
822
|
+
|
|
823
|
+
// Remove after a specific date (max 4 hours)
|
|
824
|
+
let removalDate = Date().addingTimeInterval(3600)
|
|
825
|
+
await activity.end(finalContent, dismissalPolicy: .after(removalDate))
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
## Querying Active Widgets and Activities
|
|
829
|
+
|
|
830
|
+
### Current Widget Configurations
|
|
831
|
+
|
|
832
|
+
```swift
|
|
833
|
+
let widgets = try await WidgetCenter.shared.currentConfigurations()
|
|
834
|
+
for widget in widgets {
|
|
835
|
+
print("Kind: \(widget.kind), Family: \(widget.family)")
|
|
836
|
+
}
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
### Current Live Activities
|
|
840
|
+
|
|
841
|
+
```swift
|
|
842
|
+
let activities = Activity<DeliveryAttributes>.activities
|
|
843
|
+
for activity in activities {
|
|
844
|
+
print("ID: \(activity.id), State: \(activity.activityState)")
|
|
845
|
+
}
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
### Observing New Activities
|
|
849
|
+
|
|
850
|
+
```swift
|
|
851
|
+
Task {
|
|
852
|
+
for await activity in Activity<DeliveryAttributes>.activityUpdates {
|
|
853
|
+
print("New activity started: \(activity.id)")
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
## Apple Documentation Links
|
|
859
|
+
|
|
860
|
+
- [WidgetKit](https://sosumi.ai/documentation/widgetkit)
|
|
861
|
+
- [ActivityKit](https://sosumi.ai/documentation/activitykit)
|
|
862
|
+
- [TimelineProvider](https://sosumi.ai/documentation/widgetkit/timelineprovider)
|
|
863
|
+
- [AppIntentTimelineProvider](https://sosumi.ai/documentation/widgetkit/appintenttimelineprovider)
|
|
864
|
+
- [ActivityAttributes](https://sosumi.ai/documentation/activitykit/activityattributes)
|
|
865
|
+
- [ActivityConfiguration](https://sosumi.ai/documentation/widgetkit/activityconfiguration)
|
|
866
|
+
- [DynamicIsland](https://sosumi.ai/documentation/widgetkit/dynamicisland)
|
|
867
|
+
- [ControlWidgetButton](https://sosumi.ai/documentation/widgetkit/controlwidgetbutton)
|
|
868
|
+
- [ControlWidgetToggle](https://sosumi.ai/documentation/widgetkit/controlwidgettoggle)
|
|
869
|
+
- [Keeping a widget up to date](https://sosumi.ai/documentation/widgetkit/keeping-a-widget-up-to-date)
|
|
870
|
+
- [Adding StandBy and CarPlay support](https://sosumi.ai/documentation/widgetkit/adding-standby-and-carplay-support-to-your-widget)
|
|
871
|
+
- [Optimizing for accented rendering and Liquid Glass](https://sosumi.ai/documentation/widgetkit/optimizing-your-widget-for-accented-rendering-mode-and-liquid-glass)
|