@mmerterden/multi-agent-pipeline 20.1.0 → 20.2.0
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/CHANGELOG.md +26 -0
- package/docs/facts.json +5 -5
- package/manifest.json +52 -31
- package/package.json +1 -1
- package/pipeline/skills/.skill-manifest.json +36 -20
- package/pipeline/skills/.skills-index.json +75 -9
- package/pipeline/skills/shared/README.md +13 -7
- package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
- package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
- package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
- package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
- package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
- package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
- package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
- package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
- package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
- package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
- package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
- package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
- package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
- package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
- package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
- package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
- package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
- package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
- package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
- package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
- package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
- package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
- package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
- package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
- package/pipeline/skills/skills-index.md +8 -2
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# StoreKit 2 Core Patterns
|
|
2
|
+
|
|
3
|
+
Full code for the core purchase, transaction listener, entitlement, and store
|
|
4
|
+
view flows described in the top-level StoreKit skill, plus the annotated common
|
|
5
|
+
mistakes. Load this when you need a complete, copyable example for a core flow;
|
|
6
|
+
SKILL.md keeps the review guidance and short snippets. For control styles,
|
|
7
|
+
offers, testing, server validation, and advanced subscription handling see
|
|
8
|
+
[storekit-advanced.md](storekit-advanced.md).
|
|
9
|
+
|
|
10
|
+
## Contents
|
|
11
|
+
|
|
12
|
+
- [Purchase Flow](#purchase-flow)
|
|
13
|
+
- [Transaction.updates Listener](#transactionupdates-listener)
|
|
14
|
+
- [Entitlement Checking](#entitlement-checking)
|
|
15
|
+
- [SubscriptionStoreView](#subscriptionstoreview)
|
|
16
|
+
- [StoreView and ProductView](#storeview-and-productview)
|
|
17
|
+
- [Subscription Status Checking](#subscription-status-checking)
|
|
18
|
+
- [App Transaction](#app-transaction)
|
|
19
|
+
- [SwiftUI Purchase Callbacks](#swiftui-purchase-callbacks)
|
|
20
|
+
- [Common Mistakes](#common-mistakes)
|
|
21
|
+
|
|
22
|
+
## Purchase Flow
|
|
23
|
+
|
|
24
|
+
Handle every `PurchaseResult`, verify before access, deliver durably, then
|
|
25
|
+
finish.
|
|
26
|
+
|
|
27
|
+
```swift
|
|
28
|
+
@Environment(\.purchase) private var purchase
|
|
29
|
+
|
|
30
|
+
func purchaseProduct(_ product: Product) async throws {
|
|
31
|
+
let result = try await purchase(product, options: [
|
|
32
|
+
.appAccountToken(userAccountToken)
|
|
33
|
+
])
|
|
34
|
+
switch result {
|
|
35
|
+
case .success(let verification):
|
|
36
|
+
let transaction = try checkVerified(verification)
|
|
37
|
+
await deliverContent(for: transaction)
|
|
38
|
+
await transaction.finish()
|
|
39
|
+
case .userCancelled:
|
|
40
|
+
break
|
|
41
|
+
case .pending:
|
|
42
|
+
// Ask to Buy or deferred approval: show pending UI, no unlock yet.
|
|
43
|
+
showPendingApprovalMessage()
|
|
44
|
+
@unknown default:
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
|
|
50
|
+
switch result {
|
|
51
|
+
case .verified(let value): return value
|
|
52
|
+
case .unverified(_, let error): throw error
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Transaction.updates Listener
|
|
58
|
+
|
|
59
|
+
Start at app launch, not when a paywall appears. Keep the task retained for the
|
|
60
|
+
app lifetime.
|
|
61
|
+
|
|
62
|
+
```swift
|
|
63
|
+
@main
|
|
64
|
+
struct MyApp: App {
|
|
65
|
+
private let transactionListener: Task<Void, Never>
|
|
66
|
+
|
|
67
|
+
init() {
|
|
68
|
+
transactionListener = Self.listenForTransactions()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var body: some Scene {
|
|
72
|
+
WindowGroup { ContentView() }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static func listenForTransactions() -> Task<Void, Never> {
|
|
76
|
+
Task(priority: .background) {
|
|
77
|
+
for await result in Transaction.updates {
|
|
78
|
+
guard case .verified(let transaction) = result else { continue }
|
|
79
|
+
await StoreManager.shared.updateEntitlements()
|
|
80
|
+
await transaction.finish()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Entitlement Checking
|
|
88
|
+
|
|
89
|
+
Use `Transaction.currentEntitlements`; skip transactions with a non-nil
|
|
90
|
+
`revocationDate`. Track consumable fulfillment in your own app or server ledger.
|
|
91
|
+
|
|
92
|
+
```swift
|
|
93
|
+
@Observable
|
|
94
|
+
@MainActor
|
|
95
|
+
class StoreManager {
|
|
96
|
+
static let shared = StoreManager()
|
|
97
|
+
var purchasedProductIDs: Set<String> = []
|
|
98
|
+
var isPremium: Bool { purchasedProductIDs.contains(ProductID.premium) }
|
|
99
|
+
|
|
100
|
+
func updateEntitlements() async {
|
|
101
|
+
var purchased = Set<String>()
|
|
102
|
+
for await result in Transaction.currentEntitlements {
|
|
103
|
+
if case .verified(let transaction) = result,
|
|
104
|
+
transaction.revocationDate == nil {
|
|
105
|
+
purchased.insert(transaction.productID)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
purchasedProductIDs = purchased
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### SwiftUI .currentEntitlementTask Modifier
|
|
114
|
+
|
|
115
|
+
```swift
|
|
116
|
+
struct PremiumGatedView: View {
|
|
117
|
+
@State private var state: EntitlementTaskState<VerificationResult<Transaction>?> = .loading
|
|
118
|
+
|
|
119
|
+
var body: some View {
|
|
120
|
+
Group {
|
|
121
|
+
switch state {
|
|
122
|
+
case .loading: ProgressView()
|
|
123
|
+
case .failure: PaywallView()
|
|
124
|
+
case .success(.some(.verified(let transaction))) where transaction.revocationDate == nil:
|
|
125
|
+
PremiumContentView()
|
|
126
|
+
case .success:
|
|
127
|
+
PaywallView()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
.currentEntitlementTask(for: ProductID.premium) { state in
|
|
131
|
+
self.state = state
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## SubscriptionStoreView
|
|
138
|
+
|
|
139
|
+
Built-in SwiftUI view for subscription paywalls. Handles product loading,
|
|
140
|
+
purchase UI, and restore purchases automatically.
|
|
141
|
+
|
|
142
|
+
```swift
|
|
143
|
+
SubscriptionStoreView(groupID: "YOUR_GROUP_ID")
|
|
144
|
+
.subscriptionStoreControlStyle(.prominentPicker)
|
|
145
|
+
.subscriptionStoreButtonLabel(.multiline)
|
|
146
|
+
.storeButton(.visible, for: .restorePurchases)
|
|
147
|
+
.storeButton(.visible, for: .redeemCode)
|
|
148
|
+
.subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
|
|
149
|
+
.subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)
|
|
150
|
+
.onInAppPurchaseCompletion { product, result in
|
|
151
|
+
if case .success(.success(.verified(let transaction))) = result {
|
|
152
|
+
await deliverContent(for: transaction)
|
|
153
|
+
await transaction.finish()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Custom Marketing Content
|
|
159
|
+
|
|
160
|
+
```swift
|
|
161
|
+
SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
|
|
162
|
+
VStack {
|
|
163
|
+
Image(systemName: "crown.fill").font(.system(size: 60)).foregroundStyle(.yellow)
|
|
164
|
+
Text("Unlock Premium").font(.largeTitle.bold())
|
|
165
|
+
Text("Access all features").foregroundStyle(.secondary)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
.containerBackground(.blue.gradient, for: .subscriptionStore)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
For hierarchical layouts (`SubscriptionOptionGroup`, `SubscriptionOptionSection`,
|
|
172
|
+
`SubscriptionPeriodGroupSet`) and control styles see
|
|
173
|
+
[storekit-advanced.md](storekit-advanced.md).
|
|
174
|
+
|
|
175
|
+
## StoreView and ProductView
|
|
176
|
+
|
|
177
|
+
Merchandise multiple products with localized names, prices, and purchase buttons.
|
|
178
|
+
|
|
179
|
+
```swift
|
|
180
|
+
StoreView(ids: [ProductID.gems100, ProductID.premium], prefersPromotionalIcon: true)
|
|
181
|
+
.productViewStyle(.large)
|
|
182
|
+
.storeButton(.visible, for: .restorePurchases)
|
|
183
|
+
.onInAppPurchaseCompletion { product, result in
|
|
184
|
+
if case .success(.success(.verified(let transaction))) = result {
|
|
185
|
+
await deliverContent(for: transaction)
|
|
186
|
+
await transaction.finish()
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
```swift
|
|
192
|
+
ProductView(id: ProductID.premium) { iconPhase in
|
|
193
|
+
switch iconPhase {
|
|
194
|
+
case .success(let image): image.resizable().scaledToFit()
|
|
195
|
+
case .loading: ProgressView()
|
|
196
|
+
default: Image(systemName: "star.fill")
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
.productViewStyle(.large)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Subscription Status Checking
|
|
203
|
+
|
|
204
|
+
```swift
|
|
205
|
+
func checkSubscriptionActive(groupID: String) async throws -> Bool {
|
|
206
|
+
let statuses = try await Product.SubscriptionInfo.status(for: groupID)
|
|
207
|
+
for status in statuses {
|
|
208
|
+
guard case .verified = status.renewalInfo,
|
|
209
|
+
case .verified = status.transaction else { continue }
|
|
210
|
+
if status.state == .subscribed || status.state == .inGracePeriod {
|
|
211
|
+
return true
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## App Transaction
|
|
219
|
+
|
|
220
|
+
Verify the legitimacy of the app installation. Use for business model changes
|
|
221
|
+
or detecting tampered installations (iOS 16+).
|
|
222
|
+
|
|
223
|
+
```swift
|
|
224
|
+
func verifyAppPurchase() async {
|
|
225
|
+
do {
|
|
226
|
+
let result = try await AppTransaction.shared
|
|
227
|
+
switch result {
|
|
228
|
+
case .verified(let appTransaction):
|
|
229
|
+
let originalVersion = appTransaction.originalAppVersion
|
|
230
|
+
let purchaseDate = appTransaction.originalPurchaseDate
|
|
231
|
+
// Migration logic for users who paid before subscription model
|
|
232
|
+
case .unverified:
|
|
233
|
+
// Potentially tampered -- restrict features as appropriate
|
|
234
|
+
break
|
|
235
|
+
}
|
|
236
|
+
} catch { /* Could not retrieve app transaction */ }
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## SwiftUI Purchase Callbacks
|
|
241
|
+
|
|
242
|
+
```swift
|
|
243
|
+
.onInAppPurchaseStart { product in
|
|
244
|
+
await analytics.trackPurchaseStarted(product.id)
|
|
245
|
+
}
|
|
246
|
+
.onInAppPurchaseCompletion { product, result in
|
|
247
|
+
if case .success(.success(.verified(let transaction))) = result {
|
|
248
|
+
await deliverContent(for: transaction)
|
|
249
|
+
await transaction.finish()
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
.inAppPurchaseOptions { product in
|
|
253
|
+
[.appAccountToken(userAccountToken)]
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Common Mistakes
|
|
258
|
+
|
|
259
|
+
### 1. Not starting Transaction.updates at app launch
|
|
260
|
+
|
|
261
|
+
```swift
|
|
262
|
+
// WRONG: No listener -- misses renewals, refunds, Ask to Buy approvals
|
|
263
|
+
@main struct MyApp: App {
|
|
264
|
+
var body: some Scene { WindowGroup { ContentView() } }
|
|
265
|
+
}
|
|
266
|
+
// CORRECT: Start listener in App init (see Transaction.updates Listener above)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### 2. Forgetting transaction.finish()
|
|
270
|
+
|
|
271
|
+
```swift
|
|
272
|
+
// WRONG: Never finished -- reappears in unfinished queue forever
|
|
273
|
+
let transaction = try checkVerified(verification)
|
|
274
|
+
unlockFeature(transaction.productID)
|
|
275
|
+
|
|
276
|
+
// CORRECT: Deliver durably, then finish. If delivery fails, do not finish yet.
|
|
277
|
+
let transaction = try checkVerified(verification)
|
|
278
|
+
try await recordDelivery(transaction)
|
|
279
|
+
await transaction.finish()
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 3. Ignoring verification result
|
|
283
|
+
|
|
284
|
+
```swift
|
|
285
|
+
// WRONG: Using unverified transaction -- security risk
|
|
286
|
+
let transaction = verification.unsafePayloadValue
|
|
287
|
+
|
|
288
|
+
// CORRECT: Verify before using
|
|
289
|
+
let transaction = try checkVerified(verification)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### 4. Using original In-App Purchase APIs in new StoreKit 2 code
|
|
293
|
+
|
|
294
|
+
```swift
|
|
295
|
+
// AVOID: Original In-App Purchase APIs
|
|
296
|
+
let request = SKProductsRequest(productIdentifiers: ["com.app.premium"])
|
|
297
|
+
SKPaymentQueue.default().add(payment)
|
|
298
|
+
|
|
299
|
+
// PREFERRED: StoreKit 2
|
|
300
|
+
let products = try await Product.products(for: ["com.app.premium"])
|
|
301
|
+
let result = try await product.purchase()
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### 5. Not checking revocationDate
|
|
305
|
+
|
|
306
|
+
```swift
|
|
307
|
+
// WRONG: Grants access to refunded purchases
|
|
308
|
+
if case .verified(let transaction) = result {
|
|
309
|
+
purchased.insert(transaction.productID)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// CORRECT: Skip revoked transactions
|
|
313
|
+
if case .verified(let transaction) = result, transaction.revocationDate == nil {
|
|
314
|
+
purchased.insert(transaction.productID)
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### 6. Hardcoding prices
|
|
319
|
+
|
|
320
|
+
```swift
|
|
321
|
+
// WRONG: Wrong for other currencies and regions
|
|
322
|
+
Text("Buy Premium for $4.99")
|
|
323
|
+
|
|
324
|
+
// CORRECT: Localized price from Product
|
|
325
|
+
Text("Buy \(product.displayName) for \(product.displayPrice)")
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### 7. Not handling .pending purchase result
|
|
329
|
+
|
|
330
|
+
```swift
|
|
331
|
+
// WRONG: Silently drops pending Ask to Buy
|
|
332
|
+
default: break
|
|
333
|
+
|
|
334
|
+
// CORRECT: Explain approval is pending; unlock only after Transaction.updates
|
|
335
|
+
case .pending:
|
|
336
|
+
showPendingApprovalMessage()
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### 8. Checking entitlements only once at launch
|
|
340
|
+
|
|
341
|
+
```swift
|
|
342
|
+
// WRONG: Check once, never update
|
|
343
|
+
func appDidFinish() { Task { await updateEntitlements() } }
|
|
344
|
+
|
|
345
|
+
// CORRECT: Re-check on Transaction.updates AND on foreground return
|
|
346
|
+
// Transaction.updates listener handles mid-session changes.
|
|
347
|
+
// Also use .task { await storeManager.updateEntitlements() } on content views.
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### 9. Missing restore purchases button
|
|
351
|
+
|
|
352
|
+
```swift
|
|
353
|
+
// WRONG: No restore option -- App Store rejection risk
|
|
354
|
+
SubscriptionStoreView(groupID: "group_id")
|
|
355
|
+
|
|
356
|
+
// CORRECT
|
|
357
|
+
SubscriptionStoreView(groupID: "group_id")
|
|
358
|
+
.storeButton(.visible, for: .restorePurchases)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### 10. Subscription views without policy links
|
|
362
|
+
|
|
363
|
+
```swift
|
|
364
|
+
// WRONG: No terms or privacy policy
|
|
365
|
+
SubscriptionStoreView(groupID: "group_id")
|
|
366
|
+
|
|
367
|
+
// CORRECT
|
|
368
|
+
SubscriptionStoreView(groupID: "group_id")
|
|
369
|
+
.subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
|
|
370
|
+
.subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)
|
|
371
|
+
```
|
|
@@ -225,18 +225,9 @@ struct ToggleFavoriteIntent: AppIntent {
|
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
HStack {
|
|
232
|
-
Text(entry.itemName)
|
|
233
|
-
Spacer()
|
|
234
|
-
Button(intent: ToggleFavoriteIntent(itemID: entry.itemID)) {
|
|
235
|
-
Image(systemName: entry.isFavorite ? "star.fill" : "star")
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
.padding()
|
|
239
|
-
}
|
|
228
|
+
// In the widget view:
|
|
229
|
+
Button(intent: ToggleFavoriteIntent(itemID: entry.itemID)) {
|
|
230
|
+
Image(systemName: entry.isFavorite ? "star.fill" : "star")
|
|
240
231
|
}
|
|
241
232
|
```
|
|
242
233
|
|
|
@@ -261,53 +252,11 @@ struct DeliveryAttributes: ActivityAttributes {
|
|
|
261
252
|
|
|
262
253
|
### ActivityConfiguration
|
|
263
254
|
|
|
264
|
-
Provide Lock Screen content and Dynamic Island closures in the widget bundle.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
ActivityConfiguration(for: DeliveryAttributes.self) { context in
|
|
270
|
-
VStack(alignment: .leading) {
|
|
271
|
-
Text(context.attributes.restaurantName).font(.headline)
|
|
272
|
-
HStack {
|
|
273
|
-
Text("Driver: \(context.state.driverName)")
|
|
274
|
-
Spacer()
|
|
275
|
-
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
.padding()
|
|
279
|
-
} dynamicIsland: { context in
|
|
280
|
-
DynamicIsland {
|
|
281
|
-
DynamicIslandExpandedRegion(.leading) {
|
|
282
|
-
Image(systemName: "box.truck.fill").font(.title2)
|
|
283
|
-
}
|
|
284
|
-
DynamicIslandExpandedRegion(.trailing) {
|
|
285
|
-
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
|
|
286
|
-
.font(.caption)
|
|
287
|
-
}
|
|
288
|
-
DynamicIslandExpandedRegion(.center) {
|
|
289
|
-
Text(context.attributes.restaurantName).font(.headline)
|
|
290
|
-
}
|
|
291
|
-
DynamicIslandExpandedRegion(.bottom) {
|
|
292
|
-
HStack {
|
|
293
|
-
ForEach(DeliveryStep.allCases, id: \.self) { step in
|
|
294
|
-
Image(systemName: step.icon)
|
|
295
|
-
.foregroundStyle(step <= context.state.currentStep ? .primary : .tertiary)
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
} compactLeading: {
|
|
300
|
-
Image(systemName: "box.truck.fill")
|
|
301
|
-
} compactTrailing: {
|
|
302
|
-
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
|
|
303
|
-
.frame(width: 40).monospacedDigit()
|
|
304
|
-
} minimal: {
|
|
305
|
-
Image(systemName: "box.truck.fill")
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
```
|
|
255
|
+
Provide Lock Screen content and Dynamic Island closures in the widget bundle. The
|
|
256
|
+
`ActivityConfiguration(for:)` body returns the Lock Screen view; its
|
|
257
|
+
`dynamicIsland` closure declares the expanded regions plus `compactLeading`,
|
|
258
|
+
`compactTrailing`, and `minimal`. See [references/widgetkit-advanced.md](references/widgetkit-advanced.md)
|
|
259
|
+
("Live Activity Full ActivityConfiguration Example") for a complete implementation.
|
|
311
260
|
|
|
312
261
|
### Dynamic Island Regions
|
|
313
262
|
|
|
@@ -339,8 +288,11 @@ await activity.end(final, dismissalPolicy: .after(.now.addingTimeInterval(3600))
|
|
|
339
288
|
|
|
340
289
|
## Control Center Widgets (iOS 18+)
|
|
341
290
|
|
|
291
|
+
Wrap a `ControlWidgetButton` or `ControlWidgetToggle` in a
|
|
292
|
+
`StaticControlConfiguration` (or `AppIntentControlConfiguration`). Each control
|
|
293
|
+
runs an `AppIntent` action.
|
|
294
|
+
|
|
342
295
|
```swift
|
|
343
|
-
// Button control
|
|
344
296
|
struct OpenCameraControl: ControlWidget {
|
|
345
297
|
var body: some ControlWidgetConfiguration {
|
|
346
298
|
StaticControlConfiguration(kind: "OpenCamera") {
|
|
@@ -351,41 +303,18 @@ struct OpenCameraControl: ControlWidget {
|
|
|
351
303
|
.displayName("Open Camera")
|
|
352
304
|
}
|
|
353
305
|
}
|
|
354
|
-
|
|
355
|
-
// Toggle control with value provider
|
|
356
|
-
struct FlashlightControl: ControlWidget {
|
|
357
|
-
var body: some ControlWidgetConfiguration {
|
|
358
|
-
StaticControlConfiguration(kind: "Flashlight", provider: FlashlightValueProvider()) { value in
|
|
359
|
-
ControlWidgetToggle(isOn: value, action: ToggleFlashlightIntent()) {
|
|
360
|
-
Label("Flashlight", systemImage: value ? "flashlight.on.fill" : "flashlight.off.fill")
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
.displayName("Flashlight")
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
306
|
```
|
|
367
307
|
|
|
368
|
-
|
|
308
|
+
See [references/widgetkit-advanced.md](references/widgetkit-advanced.md) ("Control Center Control
|
|
309
|
+
Examples") for a toggle control backed by a value provider.
|
|
369
310
|
|
|
370
|
-
|
|
311
|
+
## Lock Screen Widgets
|
|
371
312
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
ZStack {
|
|
378
|
-
AccessoryWidgetBackground()
|
|
379
|
-
VStack {
|
|
380
|
-
Image(systemName: "figure.walk")
|
|
381
|
-
Text("\(entry.stepCount)").font(.headline)
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline])
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
```
|
|
313
|
+
Use accessory families (`.accessoryCircular`, `.accessoryRectangular`,
|
|
314
|
+
`.accessoryInline`) and wrap content in `AccessoryWidgetBackground()` for the
|
|
315
|
+
standard translucent backing. See [references/widgetkit-advanced.md](references/widgetkit-advanced.md)
|
|
316
|
+
("Lock Screen Accessory Widget Example") for a full widget, and
|
|
317
|
+
"AccessoryWidgetBackground" for rendering-mode adaptation.
|
|
389
318
|
|
|
390
319
|
## StandBy Mode
|
|
391
320
|
|
|
@@ -430,16 +359,11 @@ Adapt widgets to the Liquid Glass visual style using `WidgetAccentedRenderingMod
|
|
|
430
359
|
|
|
431
360
|
### WidgetPushHandler
|
|
432
361
|
|
|
433
|
-
Enable push-based timeline reloads without scheduled polling
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
let tokenString = pushInfo.token.map { String(format: "%02x", $0) }.joined()
|
|
439
|
-
// Send tokenString to your server
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
```
|
|
362
|
+
Enable push-based timeline reloads without scheduled polling: implement
|
|
363
|
+
`WidgetPushHandler.pushTokenDidChange(_:widgets:)`, register the token with your
|
|
364
|
+
server, and send an APNs push to trigger a reload. See
|
|
365
|
+
[references/widgetkit-advanced.md](references/widgetkit-advanced.md) ("Push-Based Timeline Reloads") for
|
|
366
|
+
the handler plus the `ControlPushHandler` equivalent.
|
|
443
367
|
|
|
444
368
|
### CarPlay Widgets
|
|
445
369
|
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
- [Multiple Widget Support in WidgetBundle](#multiple-widget-support-in-widgetbundle)
|
|
10
10
|
- [Widget Previews and Snapshots](#widget-previews-and-snapshots)
|
|
11
11
|
- [AccessoryWidgetBackground](#accessorywidgetbackground)
|
|
12
|
+
- [Lock Screen Accessory Widget Example](#lock-screen-accessory-widget-example)
|
|
13
|
+
- [Live Activity Full ActivityConfiguration Example](#live-activity-full-activityconfiguration-example)
|
|
14
|
+
- [Control Center Control Examples](#control-center-control-examples)
|
|
12
15
|
- [Dynamic Island Expanded Layout Patterns](#dynamic-island-expanded-layout-patterns)
|
|
13
16
|
- [Alert Configuration for Live Activities](#alert-configuration-for-live-activities)
|
|
14
17
|
- [Push Notification Support for Live Activities](#push-notification-support-for-live-activities)
|
|
@@ -390,6 +393,110 @@ var body: some View {
|
|
|
390
393
|
Use `.widgetAccentable()` to mark views that should receive the accent tint in
|
|
391
394
|
`.accented` rendering mode.
|
|
392
395
|
|
|
396
|
+
## Lock Screen Accessory Widget Example
|
|
397
|
+
|
|
398
|
+
A full Lock Screen widget using accessory families and `AccessoryWidgetBackground`.
|
|
399
|
+
|
|
400
|
+
```swift
|
|
401
|
+
struct StepsWidget: Widget {
|
|
402
|
+
let kind = "StepsWidget"
|
|
403
|
+
var body: some WidgetConfiguration {
|
|
404
|
+
StaticConfiguration(kind: kind, provider: StepsProvider()) { entry in
|
|
405
|
+
ZStack {
|
|
406
|
+
AccessoryWidgetBackground()
|
|
407
|
+
VStack {
|
|
408
|
+
Image(systemName: "figure.walk")
|
|
409
|
+
Text("\(entry.stepCount)").font(.headline)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline])
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Live Activity Full ActivityConfiguration Example
|
|
419
|
+
|
|
420
|
+
A complete `ActivityConfiguration` with Lock Screen content and the Dynamic
|
|
421
|
+
Island closures (expanded regions, compact, minimal).
|
|
422
|
+
|
|
423
|
+
```swift
|
|
424
|
+
struct DeliveryActivityWidget: Widget {
|
|
425
|
+
var body: some WidgetConfiguration {
|
|
426
|
+
ActivityConfiguration(for: DeliveryAttributes.self) { context in
|
|
427
|
+
VStack(alignment: .leading) {
|
|
428
|
+
Text(context.attributes.restaurantName).font(.headline)
|
|
429
|
+
HStack {
|
|
430
|
+
Text("Driver: \(context.state.driverName)")
|
|
431
|
+
Spacer()
|
|
432
|
+
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
.padding()
|
|
436
|
+
} dynamicIsland: { context in
|
|
437
|
+
DynamicIsland {
|
|
438
|
+
DynamicIslandExpandedRegion(.leading) {
|
|
439
|
+
Image(systemName: "box.truck.fill").font(.title2)
|
|
440
|
+
}
|
|
441
|
+
DynamicIslandExpandedRegion(.trailing) {
|
|
442
|
+
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
|
|
443
|
+
.font(.caption)
|
|
444
|
+
}
|
|
445
|
+
DynamicIslandExpandedRegion(.center) {
|
|
446
|
+
Text(context.attributes.restaurantName).font(.headline)
|
|
447
|
+
}
|
|
448
|
+
DynamicIslandExpandedRegion(.bottom) {
|
|
449
|
+
HStack {
|
|
450
|
+
ForEach(DeliveryStep.allCases, id: \.self) { step in
|
|
451
|
+
Image(systemName: step.icon)
|
|
452
|
+
.foregroundStyle(step <= context.state.currentStep ? .primary : .tertiary)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
} compactLeading: {
|
|
457
|
+
Image(systemName: "box.truck.fill")
|
|
458
|
+
} compactTrailing: {
|
|
459
|
+
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
|
|
460
|
+
.frame(width: 40).monospacedDigit()
|
|
461
|
+
} minimal: {
|
|
462
|
+
Image(systemName: "box.truck.fill")
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
## Control Center Control Examples
|
|
470
|
+
|
|
471
|
+
Button and toggle controls for Control Center (iOS 18+). A toggle can read its
|
|
472
|
+
current state from a value provider.
|
|
473
|
+
|
|
474
|
+
```swift
|
|
475
|
+
// Button control
|
|
476
|
+
struct OpenCameraControl: ControlWidget {
|
|
477
|
+
var body: some ControlWidgetConfiguration {
|
|
478
|
+
StaticControlConfiguration(kind: "OpenCamera") {
|
|
479
|
+
ControlWidgetButton(action: OpenCameraIntent()) {
|
|
480
|
+
Label("Camera", systemImage: "camera.fill")
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
.displayName("Open Camera")
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Toggle control with value provider
|
|
488
|
+
struct FlashlightControl: ControlWidget {
|
|
489
|
+
var body: some ControlWidgetConfiguration {
|
|
490
|
+
StaticControlConfiguration(kind: "Flashlight", provider: FlashlightValueProvider()) { value in
|
|
491
|
+
ControlWidgetToggle(isOn: value, action: ToggleFlashlightIntent()) {
|
|
492
|
+
Label("Flashlight", systemImage: value ? "flashlight.on.fill" : "flashlight.off.fill")
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
.displayName("Flashlight")
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
393
500
|
## Dynamic Island Expanded Layout Patterns
|
|
394
501
|
|
|
395
502
|
### Full Layout Example
|