@mmerterden/multi-agent-pipeline 20.36.0 → 20.37.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 +12 -0
- package/docs/facts.json +1 -1
- package/install/catalog-history.json +1 -1
- package/manifest.json +194 -122
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +33 -154
- package/pipeline/commands/multi-agent/analysis/SKILL.md +14 -68
- package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +5 -8
- package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +38 -109
- package/pipeline/commands/multi-agent/channels/SKILL.md +38 -167
- package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +37 -144
- package/pipeline/commands/multi-agent/design-check/SKILL.md +27 -131
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +22 -48
- package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +37 -180
- package/pipeline/commands/multi-agent/jira/SKILL.md +16 -46
- package/pipeline/commands/multi-agent/refactor/SKILL.md +23 -184
- package/pipeline/commands/multi-agent/resume/SKILL.md +30 -136
- package/pipeline/commands/multi-agent/review/SKILL.md +24 -199
- package/pipeline/commands/multi-agent/setup/SKILL.md +25 -181
- package/pipeline/commands/multi-agent/stack/SKILL.md +9 -53
- package/pipeline/commands/multi-agent/status/SKILL.md +13 -2
- package/pipeline/commands/multi-agent/store-ready/SKILL.md +27 -226
- package/pipeline/commands/multi-agent/sync/SKILL.md +35 -164
- package/pipeline/commands/multi-agent/test-gen/SKILL.md +2 -4
- package/pipeline/commands/multi-agent/update/SKILL.md +18 -60
- package/pipeline/contract/CHANGELOG.md +9 -0
- package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
- package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
- package/pipeline/contract/fixtures/error-unsigned.json +1 -1
- package/pipeline/contract/fixtures/runs-awaiting-question.json +1 -1
- package/pipeline/contract/fixtures/runs-empty.json +1 -1
- package/pipeline/contract/fixtures/runs-failed.json +1 -1
- package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +1 -1
- package/pipeline/contract/fixtures/runs-pr-opened.json +1 -1
- package/pipeline/contract/fixtures/runs-running.json +1 -1
- package/pipeline/contract/manifest.json +5 -5
- package/pipeline/contract/types/index.d.ts +3 -2
- package/pipeline/multi-agent-refs/autopilot-on/details.md +148 -0
- package/pipeline/multi-agent-refs/channels/generator.md +14 -0
- package/pipeline/multi-agent-refs/channels/preview.md +13 -0
- package/pipeline/multi-agent-refs/channels/reference.md +46 -0
- package/pipeline/multi-agent-refs/complaint-analysis/phases.md +159 -0
- package/pipeline/multi-agent-refs/design-check/export.md +1 -0
- package/pipeline/multi-agent-refs/design-check/init-inventory.md +1 -1
- package/pipeline/multi-agent-refs/design-check/scope-and-ledger.md +64 -0
- package/pipeline/multi-agent-refs/features/analysis-command-notes.md +53 -0
- package/pipeline/multi-agent-refs/garbage-collect/phases.md +60 -0
- package/pipeline/multi-agent-refs/ios-coding-standard/plan.md +71 -0
- package/pipeline/multi-agent-refs/ios-coding-standard/resolve-and-scan.md +135 -0
- package/pipeline/multi-agent-refs/jira/picker-detail.md +71 -0
- package/pipeline/multi-agent-refs/orchestrator/copilot-pipeline.md +88 -0
- package/pipeline/multi-agent-refs/orchestrator/dispatcher.md +96 -1
- package/pipeline/multi-agent-refs/refactor/sweep.md +190 -0
- package/pipeline/multi-agent-refs/resume/details.md +115 -2
- package/pipeline/multi-agent-refs/review/inputs.md +33 -1
- package/pipeline/multi-agent-refs/review/module-guides.md +9 -1
- package/pipeline/multi-agent-refs/review/panel.md +75 -0
- package/pipeline/multi-agent-refs/review/valven.md +5 -1
- package/pipeline/multi-agent-refs/setup/hooks-and-plugins.md +4 -0
- package/pipeline/multi-agent-refs/setup/token-save-flow.md +13 -0
- package/pipeline/multi-agent-refs/stack/background.md +72 -0
- package/pipeline/multi-agent-refs/store-ready/gates.md +139 -0
- package/pipeline/multi-agent-refs/store-ready/report.md +40 -2
- package/pipeline/multi-agent-refs/sync/office.md +10 -0
- package/pipeline/multi-agent-refs/sync/overview.md +61 -0
- package/pipeline/multi-agent-refs/sync/repo.md +91 -0
- package/pipeline/multi-agent-refs/update/details.md +83 -0
- package/pipeline/schemas/runs-index.schema.json +2 -1
- package/pipeline/scripts/contract-server.mjs +1 -1
- package/pipeline/scripts/runs-index.mjs +34 -7
- package/pipeline/scripts/status-overview.mjs +401 -0
- package/pipeline/skills/.skill-manifest.json +63 -63
- package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +7 -70
- package/pipeline/skills/shared/core/apple-archive-compliance/references/examples.md +67 -0
- package/pipeline/skills/shared/core/apple-archive-compliance/references/version-history.md +8 -0
- package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +10 -72
- package/pipeline/skills/shared/core/google-play-compliance/references/examples.md +69 -0
- package/pipeline/skills/shared/core/google-play-compliance/references/notes.md +7 -0
- package/pipeline/skills/shared/core/google-play-compliance/references/version-history.md +5 -0
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +39 -262
- package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +35 -73
- package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +37 -337
- package/pipeline/skills/shared/core/multi-agent-design-check/SKILL.md +27 -122
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +8 -340
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +37 -180
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +23 -184
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +14 -82
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +25 -277
- package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +13 -2
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +73 -217
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +11 -162
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-code.md +257 -0
- package/pipeline/skills/shared/external/android-architecture/SKILL.md +7 -58
- package/pipeline/skills/shared/external/android-architecture/references/code-examples.md +68 -0
- package/pipeline/skills/shared/external/app-intents/SKILL.md +3 -45
- package/pipeline/skills/shared/external/app-intents/references/code-examples.md +54 -0
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +5 -81
- package/pipeline/skills/shared/external/app-store-optimization/references/engagement-and-localization.md +96 -0
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +6 -112
- package/pipeline/skills/shared/external/app-store-review/references/monetization-and-submission.md +138 -0
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +10 -117
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models-code.md +186 -0
- package/pipeline/skills/shared/external/authentication/SKILL.md +3 -50
- package/pipeline/skills/shared/external/authentication/references/code-examples.md +59 -0
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +3 -84
- package/pipeline/skills/shared/external/callkit-voip/references/code-examples.md +93 -0
- package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +7 -263
- package/pipeline/skills/shared/external/ci-cd-pipelines/references/pipeline-examples.md +281 -0
- package/pipeline/skills/shared/external/clean-code/SKILL.md +8 -41
- package/pipeline/skills/shared/external/clean-code/references/code-examples.md +56 -0
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +8 -146
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-code.md +214 -0
- package/pipeline/skills/shared/external/compose-components/SKILL.md +6 -80
- package/pipeline/skills/shared/external/compose-components/references/code-examples.md +89 -0
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +5 -64
- package/pipeline/skills/shared/external/compose-navigation/references/code-examples.md +70 -0
- package/pipeline/skills/shared/external/core-data/SKILL.md +9 -157
- package/pipeline/skills/shared/external/core-data/references/core-data-code.md +221 -0
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +3 -89
- package/pipeline/skills/shared/external/core-nfc/references/code-examples.md +98 -0
- package/pipeline/skills/shared/external/coreml/SKILL.md +17 -133
- package/pipeline/skills/shared/external/coreml/references/coreml-code.md +208 -0
- package/pipeline/skills/shared/external/css-modern/SKILL.md +8 -131
- package/pipeline/skills/shared/external/css-modern/references/code-examples.md +142 -0
- package/pipeline/skills/shared/external/database-patterns/SKILL.md +6 -103
- package/pipeline/skills/shared/external/database-patterns/references/orm-examples.md +112 -0
- package/pipeline/skills/shared/external/html-semantic/SKILL.md +7 -48
- package/pipeline/skills/shared/external/html-semantic/references/code-examples.md +54 -0
- package/pipeline/skills/shared/external/humanizer/SKILL.md +6 -19
- package/pipeline/skills/shared/external/humanizer/references/voice.md +24 -0
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +13 -170
- package/pipeline/skills/shared/external/ios-accessibility/references/assistive-tech-review.md +224 -0
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +2 -24
- package/pipeline/skills/shared/external/ios-localization/references/code-examples.md +30 -0
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +3 -58
- package/pipeline/skills/shared/external/ios-networking/references/code-examples.md +67 -0
- package/pipeline/skills/shared/external/ios-security/SKILL.md +4 -64
- package/pipeline/skills/shared/external/ios-security/references/code-examples.md +74 -0
- package/pipeline/skills/shared/external/live-activities/SKILL.md +4 -90
- package/pipeline/skills/shared/external/live-activities/references/code-examples.md +108 -0
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +9 -23
- package/pipeline/skills/shared/external/localization-reuse-map/references/project-settings.md +22 -0
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +5 -95
- package/pipeline/skills/shared/external/mapkit-location/references/code-examples.md +117 -0
- package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +7 -106
- package/pipeline/skills/shared/external/nextjs-app-router/references/app-router-examples.md +124 -0
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +2 -31
- package/pipeline/skills/shared/external/permissionkit/references/code-examples.md +37 -0
- package/pipeline/skills/shared/external/play-store-review/SKILL.md +6 -74
- package/pipeline/skills/shared/external/play-store-review/references/signing-and-policies.md +83 -0
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -125
- package/pipeline/skills/shared/external/push-notifications/references/notification-code.md +176 -0
- package/pipeline/skills/shared/external/python-patterns/SKILL.md +7 -158
- package/pipeline/skills/shared/external/python-patterns/references/python-examples.md +176 -0
- package/pipeline/skills/shared/external/rest-api-design/SKILL.md +7 -157
- package/pipeline/skills/shared/external/rest-api-design/references/api-examples.md +175 -0
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +11 -230
- package/pipeline/skills/shared/external/swift-architecture/references/pattern-examples.md +295 -0
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +17 -149
- package/pipeline/skills/shared/external/swift-codable/references/codable-recipes.md +207 -0
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +12 -99
- package/pipeline/skills/shared/external/swift-concurrency/references/code-examples.md +100 -0
- package/pipeline/skills/shared/external/swift-concurrency/references/swift-6-2-additions.md +18 -0
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +9 -78
- package/pipeline/skills/shared/external/swift-formatstyle/references/formatstyle-examples.md +113 -0
- package/pipeline/skills/shared/external/swift-language/SKILL.md +3 -56
- package/pipeline/skills/shared/external/swift-language/references/code-examples.md +65 -0
- package/pipeline/skills/shared/external/swift-testing-pro/SKILL.md +1 -0
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +6 -122
- package/pipeline/skills/shared/external/swiftdata/references/code-examples.md +140 -0
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +12 -139
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/interop-code.md +198 -0
- package/pipeline/skills/shared/external/tailwind-css/SKILL.md +9 -81
- package/pipeline/skills/shared/external/tailwind-css/references/code-examples.md +91 -0
- package/pipeline/skills/shared/external/testing-backend/SKILL.md +7 -207
- package/pipeline/skills/shared/external/testing-backend/references/testing-examples.md +225 -0
- package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +8 -89
- package/pipeline/skills/shared/external/typescript-patterns/references/code-examples.md +100 -0
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +2 -20
- package/pipeline/skills/shared/external/vision-framework/references/code-examples.md +26 -0
- package/pipeline/skills/shared/external/vue-composition/SKILL.md +7 -169
- package/pipeline/skills/shared/external/vue-composition/references/vue-examples.md +187 -0
- package/pipeline/skills/shared/external/web-accessibility/SKILL.md +7 -174
- package/pipeline/skills/shared/external/web-accessibility/references/a11y-examples.md +192 -0
- package/pipeline/skills/shared/external/web-performance/SKILL.md +8 -66
- package/pipeline/skills/shared/external/web-performance/references/code-examples.md +77 -0
- package/pipeline/skills/shared/external/web-testing/SKILL.md +11 -186
- package/pipeline/skills/shared/external/web-testing/references/testing-examples.md +213 -0
- /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/format-and-output.md +0 -0
- /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/publish-and-snapshot.md +0 -0
- /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/sources-and-recipes.md +0 -0
|
@@ -132,28 +132,7 @@ A SwiftUI app still needs an app delegate for this: the device token is only
|
|
|
132
132
|
delivered to `UIApplicationDelegate` callbacks. Attach one with
|
|
133
133
|
`@UIApplicationDelegateAdaptor`.
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
final class AppDelegate: NSObject, UIApplicationDelegate {
|
|
137
|
-
func application(
|
|
138
|
-
_ application: UIApplication,
|
|
139
|
-
didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? = nil
|
|
140
|
-
) -> Bool {
|
|
141
|
-
UNUserNotificationCenter.current().delegate = InboxNotificationDelegate.shared
|
|
142
|
-
return true
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
func application(_ application: UIApplication,
|
|
146
|
-
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
147
|
-
let hex = deviceToken.reduce(into: "") { $0 += String(format: "%02x", $1) }
|
|
148
|
-
Task { await DeviceTokenUploader.shared.send(hex) }
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
func application(_ application: UIApplication,
|
|
152
|
-
didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
|
153
|
-
// Expected on Simulator: it can show simulated pushes but never gets an APNs token.
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
```
|
|
135
|
+
Code for this section: [Notification code](references/notification-code.md).
|
|
157
136
|
|
|
158
137
|
### Order of calls
|
|
159
138
|
|
|
@@ -201,41 +180,11 @@ content.threadIdentifier = "garden" // groups these together in Notification C
|
|
|
201
180
|
|
|
202
181
|
### Triggers
|
|
203
182
|
|
|
204
|
-
|
|
205
|
-
// Once, 15 minutes from now. A repeating interval must be at least 60 seconds.
|
|
206
|
-
let soon = UNTimeIntervalNotificationTrigger(timeInterval: 900, repeats: false)
|
|
207
|
-
|
|
208
|
-
// Every day at 07:30.
|
|
209
|
-
var morning = DateComponents()
|
|
210
|
-
morning.hour = 7
|
|
211
|
-
morning.minute = 30
|
|
212
|
-
let daily = UNCalendarNotificationTrigger(dateMatching: morning, repeats: true)
|
|
213
|
-
|
|
214
|
-
// On arriving at a place. Needs at least When In Use location permission.
|
|
215
|
-
let gym = CLCircularRegion(
|
|
216
|
-
center: CLLocationCoordinate2D(latitude: 52.37, longitude: 4.89),
|
|
217
|
-
radius: 150,
|
|
218
|
-
identifier: "gym"
|
|
219
|
-
)
|
|
220
|
-
gym.notifyOnEntry = true
|
|
221
|
-
gym.notifyOnExit = false
|
|
222
|
-
let arrival = UNLocationNotificationTrigger(region: gym, repeats: false)
|
|
223
|
-
```
|
|
183
|
+
Code for this section: [Notification code](references/notification-code.md).
|
|
224
184
|
|
|
225
185
|
### Scheduling and cleanup
|
|
226
186
|
|
|
227
|
-
|
|
228
|
-
let center = UNUserNotificationCenter.current()
|
|
229
|
-
let request = UNNotificationRequest(identifier: "water-basil", content: content, trigger: daily)
|
|
230
|
-
try await center.add(request)
|
|
231
|
-
|
|
232
|
-
let waiting = await center.pendingNotificationRequests()
|
|
233
|
-
|
|
234
|
-
center.removePendingNotificationRequests(withIdentifiers: ["water-basil"])
|
|
235
|
-
center.removeAllPendingNotificationRequests()
|
|
236
|
-
center.removeDeliveredNotifications(withIdentifiers: ["water-basil"])
|
|
237
|
-
center.removeAllDeliveredNotifications()
|
|
238
|
-
```
|
|
187
|
+
Code for this section: [Notification code](references/notification-code.md).
|
|
239
188
|
|
|
240
189
|
## Remote Payloads
|
|
241
190
|
|
|
@@ -321,38 +270,6 @@ Let the device pick the language by sending keys instead of text:
|
|
|
321
270
|
Install `UNUserNotificationCenter.current().delegate` before launch finishes,
|
|
322
271
|
from `App.init` or from `application(_:didFinishLaunchingWithOptions:)`.
|
|
323
272
|
|
|
324
|
-
```swift
|
|
325
|
-
@MainActor
|
|
326
|
-
final class InboxNotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
|
327
|
-
static let shared = InboxNotificationDelegate()
|
|
328
|
-
|
|
329
|
-
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
|
|
330
|
-
willPresent notification: UNNotification) async
|
|
331
|
-
-> UNNotificationPresentationOptions {
|
|
332
|
-
[.banner, .sound, .badge]
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
|
|
336
|
-
didReceive response: UNNotificationResponse) async {
|
|
337
|
-
let action = response.actionIdentifier
|
|
338
|
-
let threadID = response.notification.request.content.userInfo["threadID"] as? String
|
|
339
|
-
let typed = (response as? UNTextInputNotificationResponse)?.userText
|
|
340
|
-
await route(action: action, threadID: threadID, typed: typed)
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
private func route(action: String, threadID: String?, typed: String?) async {
|
|
344
|
-
switch action {
|
|
345
|
-
case UNNotificationDefaultActionIdentifier:
|
|
346
|
-
AppRouter.shared.openThread(threadID) // tapped the notification
|
|
347
|
-
case UNNotificationDismissActionIdentifier:
|
|
348
|
-
break // swiped away
|
|
349
|
-
default:
|
|
350
|
-
await handleChatAction(action, threadID: threadID, typed: typed)
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
```
|
|
355
|
-
|
|
356
273
|
The system does not promise to call these methods on the main thread, and
|
|
357
274
|
`UNNotification` and `UNNotificationResponse` are not `Sendable`. Mark the
|
|
358
275
|
protocol methods `nonisolated`, copy the plain values you need out of the
|
|
@@ -362,6 +279,8 @@ actor.
|
|
|
362
279
|
Leave out `willPresent` and anything that arrives while your app is frontmost
|
|
363
280
|
is dropped without a banner.
|
|
364
281
|
|
|
282
|
+
Code for this section: [Notification code](references/notification-code.md).
|
|
283
|
+
|
|
365
284
|
### Deep links from a tap
|
|
366
285
|
|
|
367
286
|
Keep a single `@Observable @MainActor` router with an optional
|
|
@@ -374,29 +293,6 @@ it back to `nil`. A version that also switches tabs lives in the
|
|
|
374
293
|
|
|
375
294
|
Register categories at launch, before any notification can arrive.
|
|
376
295
|
|
|
377
|
-
```swift
|
|
378
|
-
let reply = UNTextInputNotificationAction(
|
|
379
|
-
identifier: "REPLY",
|
|
380
|
-
title: "Reply",
|
|
381
|
-
options: [],
|
|
382
|
-
textInputButtonTitle: "Send",
|
|
383
|
-
textInputPlaceholder: "Message"
|
|
384
|
-
)
|
|
385
|
-
let archive = UNNotificationAction(identifier: "ARCHIVE", title: "Archive", options: [.destructive])
|
|
386
|
-
let open = UNNotificationAction(identifier: "OPEN", title: "Open", options: [.foreground])
|
|
387
|
-
let approve = UNNotificationAction(identifier: "APPROVE", title: "Approve", options: [.authenticationRequired])
|
|
388
|
-
|
|
389
|
-
let chat = UNNotificationCategory(
|
|
390
|
-
identifier: "CHAT",
|
|
391
|
-
actions: [reply, archive],
|
|
392
|
-
intentIdentifiers: [],
|
|
393
|
-
options: [.customDismissAction] // dismissal also reaches didReceive
|
|
394
|
-
)
|
|
395
|
-
let request = UNNotificationCategory(identifier: "EXPENSE", actions: [approve, open], intentIdentifiers: [])
|
|
396
|
-
|
|
397
|
-
UNUserNotificationCenter.current().setNotificationCategories([chat, request])
|
|
398
|
-
```
|
|
399
|
-
|
|
400
296
|
| Option | Effect |
|
|
401
297
|
|--------|--------|
|
|
402
298
|
| `.authenticationRequired` | Runs only after the device is unlocked |
|
|
@@ -404,23 +300,9 @@ UNUserNotificationCenter.current().setNotificationCategories([chat, request])
|
|
|
404
300
|
| `.foreground` | Tapping it opens the app |
|
|
405
301
|
|
|
406
302
|
A text action's response arrives as a `UNTextInputNotificationResponse`; its
|
|
407
|
-
`userText` is what the delegate
|
|
303
|
+
`userText` is what the delegate passes on as `typed`.
|
|
408
304
|
|
|
409
|
-
|
|
410
|
-
@MainActor
|
|
411
|
-
func handleChatAction(_ action: String, threadID: String?, typed: String?) async {
|
|
412
|
-
guard let threadID else { return }
|
|
413
|
-
switch action {
|
|
414
|
-
case "REPLY":
|
|
415
|
-
guard let typed, !typed.isEmpty else { return }
|
|
416
|
-
await ChatService.shared.send(typed, to: threadID)
|
|
417
|
-
case "ARCHIVE":
|
|
418
|
-
await ChatService.shared.archive(threadID)
|
|
419
|
-
default:
|
|
420
|
-
break
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
```
|
|
305
|
+
Code for this section: [Notification code](references/notification-code.md).
|
|
424
306
|
|
|
425
307
|
## Grouping
|
|
426
308
|
|
|
@@ -470,6 +352,7 @@ let chat = UNNotificationCategory(
|
|
|
470
352
|
|
|
471
353
|
## Apple Documentation
|
|
472
354
|
|
|
355
|
+
- [Notification code](references/notification-code.md): open for the code behind APNs registration, local triggers, the delegate and actions and categories.
|
|
473
356
|
- [APNs registration](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns)
|
|
474
357
|
- [Requesting permission](https://developer.apple.com/documentation/usernotifications/asking-permission-to-use-notifications)
|
|
475
358
|
- [Building a remote payload](https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Notification code
|
|
2
|
+
|
|
3
|
+
<!-- toc -->
|
|
4
|
+
- [Registering with APNs](#registering-with-apns)
|
|
5
|
+
- [Triggers](#triggers)
|
|
6
|
+
- [Scheduling and cleanup](#scheduling-and-cleanup)
|
|
7
|
+
- [The delegate](#the-delegate)
|
|
8
|
+
- [Actions and Categories](#actions-and-categories)
|
|
9
|
+
<!-- /toc -->
|
|
10
|
+
|
|
11
|
+
Code for APNs registration, local triggers and scheduling, the notification delegate and actions and categories. SKILL.md keeps the rules and review guidance.
|
|
12
|
+
|
|
13
|
+
## Registering with APNs
|
|
14
|
+
|
|
15
|
+
A SwiftUI app still needs an app delegate for this: the device token is only
|
|
16
|
+
delivered to `UIApplicationDelegate` callbacks. Attach one with
|
|
17
|
+
`@UIApplicationDelegateAdaptor`.
|
|
18
|
+
|
|
19
|
+
```swift
|
|
20
|
+
final class AppDelegate: NSObject, UIApplicationDelegate {
|
|
21
|
+
func application(
|
|
22
|
+
_ application: UIApplication,
|
|
23
|
+
didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? = nil
|
|
24
|
+
) -> Bool {
|
|
25
|
+
UNUserNotificationCenter.current().delegate = InboxNotificationDelegate.shared
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func application(_ application: UIApplication,
|
|
30
|
+
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
31
|
+
let hex = deviceToken.reduce(into: "") { $0 += String(format: "%02x", $1) }
|
|
32
|
+
Task { await DeviceTokenUploader.shared.send(hex) }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func application(_ application: UIApplication,
|
|
36
|
+
didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
|
37
|
+
// Expected on Simulator: it can show simulated pushes but never gets an APNs token.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Triggers
|
|
43
|
+
|
|
44
|
+
```swift
|
|
45
|
+
// Once, 15 minutes from now. A repeating interval must be at least 60 seconds.
|
|
46
|
+
let soon = UNTimeIntervalNotificationTrigger(timeInterval: 900, repeats: false)
|
|
47
|
+
|
|
48
|
+
// Every day at 07:30.
|
|
49
|
+
var morning = DateComponents()
|
|
50
|
+
morning.hour = 7
|
|
51
|
+
morning.minute = 30
|
|
52
|
+
let daily = UNCalendarNotificationTrigger(dateMatching: morning, repeats: true)
|
|
53
|
+
|
|
54
|
+
// On arriving at a place. Needs at least When In Use location permission.
|
|
55
|
+
let gym = CLCircularRegion(
|
|
56
|
+
center: CLLocationCoordinate2D(latitude: 52.37, longitude: 4.89),
|
|
57
|
+
radius: 150,
|
|
58
|
+
identifier: "gym"
|
|
59
|
+
)
|
|
60
|
+
gym.notifyOnEntry = true
|
|
61
|
+
gym.notifyOnExit = false
|
|
62
|
+
let arrival = UNLocationNotificationTrigger(region: gym, repeats: false)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Scheduling and cleanup
|
|
66
|
+
|
|
67
|
+
```swift
|
|
68
|
+
let center = UNUserNotificationCenter.current()
|
|
69
|
+
let request = UNNotificationRequest(identifier: "water-basil", content: content, trigger: daily)
|
|
70
|
+
try await center.add(request)
|
|
71
|
+
|
|
72
|
+
let waiting = await center.pendingNotificationRequests()
|
|
73
|
+
|
|
74
|
+
center.removePendingNotificationRequests(withIdentifiers: ["water-basil"])
|
|
75
|
+
center.removeAllPendingNotificationRequests()
|
|
76
|
+
center.removeDeliveredNotifications(withIdentifiers: ["water-basil"])
|
|
77
|
+
center.removeAllDeliveredNotifications()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### The delegate
|
|
81
|
+
|
|
82
|
+
Install `UNUserNotificationCenter.current().delegate` before launch finishes,
|
|
83
|
+
from `App.init` or from `application(_:didFinishLaunchingWithOptions:)`.
|
|
84
|
+
|
|
85
|
+
```swift
|
|
86
|
+
@MainActor
|
|
87
|
+
final class InboxNotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
|
88
|
+
static let shared = InboxNotificationDelegate()
|
|
89
|
+
|
|
90
|
+
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
|
|
91
|
+
willPresent notification: UNNotification) async
|
|
92
|
+
-> UNNotificationPresentationOptions {
|
|
93
|
+
[.banner, .sound, .badge]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
|
|
97
|
+
didReceive response: UNNotificationResponse) async {
|
|
98
|
+
let action = response.actionIdentifier
|
|
99
|
+
let threadID = response.notification.request.content.userInfo["threadID"] as? String
|
|
100
|
+
let typed = (response as? UNTextInputNotificationResponse)?.userText
|
|
101
|
+
await route(action: action, threadID: threadID, typed: typed)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private func route(action: String, threadID: String?, typed: String?) async {
|
|
105
|
+
switch action {
|
|
106
|
+
case UNNotificationDefaultActionIdentifier:
|
|
107
|
+
AppRouter.shared.openThread(threadID) // tapped the notification
|
|
108
|
+
case UNNotificationDismissActionIdentifier:
|
|
109
|
+
break // swiped away
|
|
110
|
+
default:
|
|
111
|
+
await handleChatAction(action, threadID: threadID, typed: typed)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The system does not promise to call these methods on the main thread, and
|
|
118
|
+
`UNNotification` and `UNNotificationResponse` are not `Sendable`. Mark the
|
|
119
|
+
protocol methods `nonisolated`, copy the plain values you need out of the
|
|
120
|
+
response (all payload data sits in `content.userInfo`), then hop to the main
|
|
121
|
+
actor.
|
|
122
|
+
|
|
123
|
+
Leave out `willPresent` and anything that arrives while your app is frontmost
|
|
124
|
+
is dropped without a banner.
|
|
125
|
+
|
|
126
|
+
## Actions and Categories
|
|
127
|
+
|
|
128
|
+
Register categories at launch, before any notification can arrive.
|
|
129
|
+
|
|
130
|
+
```swift
|
|
131
|
+
let reply = UNTextInputNotificationAction(
|
|
132
|
+
identifier: "REPLY",
|
|
133
|
+
title: "Reply",
|
|
134
|
+
options: [],
|
|
135
|
+
textInputButtonTitle: "Send",
|
|
136
|
+
textInputPlaceholder: "Message"
|
|
137
|
+
)
|
|
138
|
+
let archive = UNNotificationAction(identifier: "ARCHIVE", title: "Archive", options: [.destructive])
|
|
139
|
+
let open = UNNotificationAction(identifier: "OPEN", title: "Open", options: [.foreground])
|
|
140
|
+
let approve = UNNotificationAction(identifier: "APPROVE", title: "Approve", options: [.authenticationRequired])
|
|
141
|
+
|
|
142
|
+
let chat = UNNotificationCategory(
|
|
143
|
+
identifier: "CHAT",
|
|
144
|
+
actions: [reply, archive],
|
|
145
|
+
intentIdentifiers: [],
|
|
146
|
+
options: [.customDismissAction] // dismissal also reaches didReceive
|
|
147
|
+
)
|
|
148
|
+
let request = UNNotificationCategory(identifier: "EXPENSE", actions: [approve, open], intentIdentifiers: [])
|
|
149
|
+
|
|
150
|
+
UNUserNotificationCenter.current().setNotificationCategories([chat, request])
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| Option | Effect |
|
|
154
|
+
|--------|--------|
|
|
155
|
+
| `.authenticationRequired` | Runs only after the device is unlocked |
|
|
156
|
+
| `.destructive` | Shown in red; for delete or remove |
|
|
157
|
+
| `.foreground` | Tapping it opens the app |
|
|
158
|
+
|
|
159
|
+
A text action's response arrives as a `UNTextInputNotificationResponse`; its
|
|
160
|
+
`userText` is what the delegate above passes on as `typed`:
|
|
161
|
+
|
|
162
|
+
```swift
|
|
163
|
+
@MainActor
|
|
164
|
+
func handleChatAction(_ action: String, threadID: String?, typed: String?) async {
|
|
165
|
+
guard let threadID else { return }
|
|
166
|
+
switch action {
|
|
167
|
+
case "REPLY":
|
|
168
|
+
guard let typed, !typed.isEmpty else { return }
|
|
169
|
+
await ChatService.shared.send(typed, to: threadID)
|
|
170
|
+
case "ARCHIVE":
|
|
171
|
+
await ChatService.shared.archive(threadID)
|
|
172
|
+
default:
|
|
173
|
+
break
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
@@ -105,117 +105,11 @@ user_set: set[User] = {user}
|
|
|
105
105
|
|
|
106
106
|
## Pydantic v2
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict
|
|
110
|
-
from datetime import datetime
|
|
111
|
-
|
|
112
|
-
class CreateUserRequest(BaseModel):
|
|
113
|
-
model_config = ConfigDict(str_strip_whitespace=True, frozen=True)
|
|
114
|
-
|
|
115
|
-
name: str = Field(min_length=1, max_length=100)
|
|
116
|
-
email: str = Field(pattern=r'^[\w.-]+@[\w.-]+\.\w+$')
|
|
117
|
-
age: int = Field(ge=0, le=150)
|
|
118
|
-
role: Role = Role.USER
|
|
119
|
-
tags: list[str] = Field(default_factory=list, max_length=10)
|
|
120
|
-
|
|
121
|
-
@field_validator('email')
|
|
122
|
-
@classmethod
|
|
123
|
-
def normalize_email(cls, v: str) -> str:
|
|
124
|
-
return v.lower()
|
|
125
|
-
|
|
126
|
-
@model_validator(mode='after')
|
|
127
|
-
def validate_admin_age(self) -> 'CreateUserRequest':
|
|
128
|
-
if self.role == Role.ADMIN and self.age < 18:
|
|
129
|
-
raise ValueError("Admin must be 18 or older")
|
|
130
|
-
return self
|
|
131
|
-
|
|
132
|
-
class UserResponse(BaseModel):
|
|
133
|
-
id: str
|
|
134
|
-
name: str
|
|
135
|
-
email: str
|
|
136
|
-
role: Role
|
|
137
|
-
created_at: datetime
|
|
138
|
-
|
|
139
|
-
model_config = ConfigDict(from_attributes=True)
|
|
140
|
-
|
|
141
|
-
# Validation
|
|
142
|
-
try:
|
|
143
|
-
user = CreateUserRequest(name="Alice", email="ALICE@example.com", age=25)
|
|
144
|
-
print(user.email) # "alice@example.com" (normalized)
|
|
145
|
-
except ValidationError as e:
|
|
146
|
-
print(e.errors())
|
|
147
|
-
|
|
148
|
-
# Serialization
|
|
149
|
-
user_dict = user.model_dump()
|
|
150
|
-
user_json = user.model_dump_json()
|
|
151
|
-
|
|
152
|
-
# From ORM object
|
|
153
|
-
db_user = db.query(UserModel).first()
|
|
154
|
-
response = UserResponse.model_validate(db_user)
|
|
155
|
-
```
|
|
108
|
+
Code for this section: [Python pattern examples](references/python-examples.md).
|
|
156
109
|
|
|
157
110
|
## Async/Await with asyncio
|
|
158
111
|
|
|
159
|
-
|
|
160
|
-
import asyncio
|
|
161
|
-
from collections.abc import AsyncIterator
|
|
162
|
-
import httpx
|
|
163
|
-
|
|
164
|
-
# Async function
|
|
165
|
-
async def fetch_user(user_id: str) -> dict:
|
|
166
|
-
async with httpx.AsyncClient() as client:
|
|
167
|
-
response = await client.get(f"https://api.example.com/users/{user_id}")
|
|
168
|
-
response.raise_for_status()
|
|
169
|
-
return response.json()
|
|
170
|
-
|
|
171
|
-
# Parallel execution
|
|
172
|
-
async def fetch_all_users(user_ids: list[str]) -> list[dict]:
|
|
173
|
-
async with httpx.AsyncClient() as client:
|
|
174
|
-
tasks = [
|
|
175
|
-
client.get(f"https://api.example.com/users/{uid}")
|
|
176
|
-
for uid in user_ids
|
|
177
|
-
]
|
|
178
|
-
responses = await asyncio.gather(*tasks, return_exceptions=True)
|
|
179
|
-
return [
|
|
180
|
-
r.json() for r in responses
|
|
181
|
-
if not isinstance(r, Exception) and r.status_code == 200
|
|
182
|
-
]
|
|
183
|
-
|
|
184
|
-
# Async context manager
|
|
185
|
-
from contextlib import asynccontextmanager
|
|
186
|
-
|
|
187
|
-
@asynccontextmanager
|
|
188
|
-
async def get_db_session() -> AsyncIterator[AsyncSession]:
|
|
189
|
-
session = AsyncSession(engine)
|
|
190
|
-
try:
|
|
191
|
-
yield session
|
|
192
|
-
await session.commit()
|
|
193
|
-
except Exception:
|
|
194
|
-
await session.rollback()
|
|
195
|
-
raise
|
|
196
|
-
finally:
|
|
197
|
-
await session.close()
|
|
198
|
-
|
|
199
|
-
# Async generator
|
|
200
|
-
async def stream_events(url: str) -> AsyncIterator[dict]:
|
|
201
|
-
async with httpx.AsyncClient() as client:
|
|
202
|
-
async with client.stream("GET", url) as response:
|
|
203
|
-
async for line in response.aiter_lines():
|
|
204
|
-
if line.startswith("data: "):
|
|
205
|
-
yield json.loads(line[6:])
|
|
206
|
-
|
|
207
|
-
# Semaphore for rate limiting
|
|
208
|
-
async def fetch_with_limit(urls: list[str], max_concurrent: int = 10) -> list[str]:
|
|
209
|
-
semaphore = asyncio.Semaphore(max_concurrent)
|
|
210
|
-
|
|
211
|
-
async def fetch_one(url: str) -> str:
|
|
212
|
-
async with semaphore:
|
|
213
|
-
async with httpx.AsyncClient() as client:
|
|
214
|
-
resp = await client.get(url)
|
|
215
|
-
return resp.text
|
|
216
|
-
|
|
217
|
-
return await asyncio.gather(*[fetch_one(url) for url in urls])
|
|
218
|
-
```
|
|
112
|
+
Code for this section: [Python pattern examples](references/python-examples.md).
|
|
219
113
|
|
|
220
114
|
## Context Managers
|
|
221
115
|
|
|
@@ -301,56 +195,7 @@ active_users = filter_active(parse_csv(read_lines("users.csv")))
|
|
|
301
195
|
|
|
302
196
|
## Pattern Matching (match/case)
|
|
303
197
|
|
|
304
|
-
|
|
305
|
-
from dataclasses import dataclass
|
|
306
|
-
|
|
307
|
-
@dataclass
|
|
308
|
-
class Point:
|
|
309
|
-
x: float
|
|
310
|
-
y: float
|
|
311
|
-
|
|
312
|
-
def describe_point(point: Point) -> str:
|
|
313
|
-
match point:
|
|
314
|
-
case Point(x=0, y=0):
|
|
315
|
-
return "origin"
|
|
316
|
-
case Point(x=0, y=y):
|
|
317
|
-
return f"on y-axis at y={y}"
|
|
318
|
-
case Point(x=x, y=0):
|
|
319
|
-
return f"on x-axis at x={x}"
|
|
320
|
-
case Point(x=x, y=y) if x == y:
|
|
321
|
-
return f"on diagonal at {x}"
|
|
322
|
-
case _:
|
|
323
|
-
return f"at ({point.x}, {point.y})"
|
|
324
|
-
|
|
325
|
-
# Matching on type and structure
|
|
326
|
-
def handle_response(response: dict) -> str:
|
|
327
|
-
match response:
|
|
328
|
-
case {"status": 200, "data": data}:
|
|
329
|
-
return f"Success: {data}"
|
|
330
|
-
case {"status": 404}:
|
|
331
|
-
return "Not found"
|
|
332
|
-
case {"status": status} if 500 <= status < 600:
|
|
333
|
-
return f"Server error: {status}"
|
|
334
|
-
case {"error": str(message)}:
|
|
335
|
-
return f"Error: {message}"
|
|
336
|
-
case _:
|
|
337
|
-
return "Unknown response"
|
|
338
|
-
|
|
339
|
-
# Matching sequences
|
|
340
|
-
def process_command(command: list[str]) -> None:
|
|
341
|
-
match command:
|
|
342
|
-
case ["quit" | "exit"]:
|
|
343
|
-
sys.exit(0)
|
|
344
|
-
case ["go", direction]:
|
|
345
|
-
move(direction)
|
|
346
|
-
case ["pick", "up", item]:
|
|
347
|
-
pick_up(item)
|
|
348
|
-
case ["drop", *items] if items:
|
|
349
|
-
for item in items:
|
|
350
|
-
drop(item)
|
|
351
|
-
case _:
|
|
352
|
-
print(f"Unknown command: {' '.join(command)}")
|
|
353
|
-
```
|
|
198
|
+
Code for this section: [Python pattern examples](references/python-examples.md).
|
|
354
199
|
|
|
355
200
|
## Do's
|
|
356
201
|
|
|
@@ -384,3 +229,7 @@ def process_command(command: list[str]) -> None:
|
|
|
384
229
|
| Type checker error with Protocol | Missing method or wrong signature | Match Protocol method signatures exactly |
|
|
385
230
|
| Slow async code | Sequential awaits instead of parallel | Use `asyncio.gather()` or `asyncio.TaskGroup` for concurrency |
|
|
386
231
|
| "TypeError: unhashable type" | Using mutable object as dict key | Use `frozen=True` dataclass or tuple instead |
|
|
232
|
+
|
|
233
|
+
## References
|
|
234
|
+
|
|
235
|
+
- [Python pattern examples](references/python-examples.md): open when writing Pydantic v2 models, asyncio concurrency or `match`/`case` code.
|