@mmerterden/multi-agent-pipeline 20.36.0 → 20.37.1
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 +37 -0
- package/docs/facts.json +2 -2
- package/docs/features.md +1 -1
- package/docs/recovery-guide.md +1 -1
- package/install/catalog-history.json +1 -1
- package/manifest.json +225 -148
- 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/manual-test/SKILL.md +12 -7
- 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/audit-guide.md +20 -0
- 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/features/commit-multi-repo.md +63 -0
- package/pipeline/multi-agent-refs/features/dev-multi-repo.md +51 -0
- package/pipeline/multi-agent-refs/features/test-gap.md +40 -0
- package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
- 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/orchestrator/phase-3-user-test.md +12 -9
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +4 -4
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +8 -47
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +21 -65
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +9 -59
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +0 -31
- 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/rules.md +1 -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/office/index.html +6 -3
- package/pipeline/office/toolkit.json +1 -1
- package/pipeline/rules/figma-pipeline.md +6 -3
- package/pipeline/schemas/prefs.schema.json +1 -1
- package/pipeline/schemas/runs-index.schema.json +2 -1
- package/pipeline/scripts/_memory-policy.mjs +27 -5
- package/pipeline/scripts/checkpoint-fold.sh +59 -0
- package/pipeline/scripts/contract-server.mjs +1 -1
- package/pipeline/scripts/gate-ledger.mjs +1 -1
- package/pipeline/scripts/office-control.mjs +41 -15
- package/pipeline/scripts/offload-ref.sh +1 -1
- package/pipeline/scripts/rework-rulings.mjs +5 -3
- package/pipeline/scripts/runs-index.mjs +34 -7
- package/pipeline/scripts/scope-check-gate.mjs +15 -2
- package/pipeline/scripts/status-overview.mjs +401 -0
- package/pipeline/scripts/user-test-handoff.sh +203 -0
- package/pipeline/scripts/validate-planning.mjs +5 -3
- package/pipeline/scripts/validate-triage.mjs +2 -1
- package/pipeline/scripts/write-state.mjs +13 -4
- package/pipeline/skills/.skill-manifest.json +64 -64
- 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-manual-test/SKILL.md +12 -7
- 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
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# AlarmKit code
|
|
2
|
+
|
|
3
|
+
<!-- toc -->
|
|
4
|
+
- [Authorization](#authorization)
|
|
5
|
+
- [Countdown timers](#countdown-timers)
|
|
6
|
+
- [Live Activity integration](#live-activity-integration)
|
|
7
|
+
<!-- /toc -->
|
|
8
|
+
|
|
9
|
+
Code for authorization, scheduling, countdown timers, presentations, metadata and the Live Activity widget. SKILL.md keeps the rules, lifecycle and review guidance.
|
|
10
|
+
|
|
11
|
+
## Authorization
|
|
12
|
+
|
|
13
|
+
The user must grant access. The app may ask up front, or AlarmKit will prompt
|
|
14
|
+
the first time something is scheduled. If neither prompt ends in approval, no
|
|
15
|
+
alarm is scheduled and nothing will ring.
|
|
16
|
+
|
|
17
|
+
```swift
|
|
18
|
+
import AlarmKit
|
|
19
|
+
|
|
20
|
+
func ensureAlarmAccess() async -> Bool {
|
|
21
|
+
let center = AlarmManager.shared
|
|
22
|
+
switch center.authorizationState {
|
|
23
|
+
case .authorized:
|
|
24
|
+
return true
|
|
25
|
+
case .denied:
|
|
26
|
+
return false
|
|
27
|
+
case .notDetermined:
|
|
28
|
+
let answer = try? await center.requestAuthorization()
|
|
29
|
+
return answer == .authorized
|
|
30
|
+
@unknown default:
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func watchAlarmAccess(onChange: @escaping (Bool) -> Void) async {
|
|
36
|
+
for await state in AlarmManager.shared.authorizationUpdates {
|
|
37
|
+
switch state {
|
|
38
|
+
case .authorized: onChange(true)
|
|
39
|
+
case .denied, .notDetermined: onChange(false)
|
|
40
|
+
@unknown default: onChange(false)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`AlarmManager.AuthorizationState` has three cases: `.notDetermined`,
|
|
47
|
+
`.denied` and `.authorized`. `authorizationState` is a synchronous read;
|
|
48
|
+
`authorizationUpdates` is an async sequence of changes.
|
|
49
|
+
|
|
50
|
+
### Configuring and scheduling
|
|
51
|
+
|
|
52
|
+
```swift
|
|
53
|
+
import AlarmKit
|
|
54
|
+
import SwiftUI
|
|
55
|
+
|
|
56
|
+
struct ShiftInfo: AlarmMetadata {
|
|
57
|
+
var shiftName: String
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@available(iOS 26.1, *)
|
|
61
|
+
func scheduleShiftAlarm() async throws -> Alarm {
|
|
62
|
+
let dawn = Alarm.Schedule.Relative.Time(hour: 5, minute: 30)
|
|
63
|
+
let alert = AlarmPresentation.Alert(
|
|
64
|
+
title: "Early shift",
|
|
65
|
+
secondaryButton: AlarmButton(text: "Later", textColor: .white, systemImageName: "zzz"),
|
|
66
|
+
secondaryButtonBehavior: .countdown
|
|
67
|
+
)
|
|
68
|
+
let attributes = AlarmAttributes(
|
|
69
|
+
presentation: AlarmPresentation(alert: alert),
|
|
70
|
+
metadata: ShiftInfo(shiftName: "Warehouse A"),
|
|
71
|
+
tintColor: .teal
|
|
72
|
+
)
|
|
73
|
+
let config = AlarmManager.AlarmConfiguration(
|
|
74
|
+
countdownDuration: Alarm.CountdownDuration(preAlert: nil, postAlert: 9 * 60),
|
|
75
|
+
schedule: .relative(.init(time: dawn, repeats: .never)),
|
|
76
|
+
attributes: attributes,
|
|
77
|
+
sound: .default
|
|
78
|
+
)
|
|
79
|
+
let center = AlarmManager.shared
|
|
80
|
+
return try await center.schedule(id: UUID(), configuration: config)
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Here `postAlert` is what makes the secondary button a snooze: pressing it
|
|
85
|
+
starts a nine-minute countdown and then rings again.
|
|
86
|
+
|
|
87
|
+
`stopIntent` and `secondaryIntent` both default to `nil`, and that is usually
|
|
88
|
+
right:
|
|
89
|
+
|
|
90
|
+
- Leave `stopIntent` out when the system Stop is all you need. Pass one only
|
|
91
|
+
when stopping must also clean up app state or trigger some side effect.
|
|
92
|
+
- Leave `secondaryIntent` out for a plain snooze or repeat, which is
|
|
93
|
+
`secondaryButtonBehavior: .countdown` plus `CountdownDuration.postAlert`.
|
|
94
|
+
Pass one only with `.custom` behavior or when the app must do its own work.
|
|
95
|
+
|
|
96
|
+
## Countdown timers
|
|
97
|
+
|
|
98
|
+
A timer rings once a duration has passed and always shows its countdown.
|
|
99
|
+
|
|
100
|
+
```swift
|
|
101
|
+
import AlarmKit
|
|
102
|
+
import SwiftUI
|
|
103
|
+
|
|
104
|
+
struct BrewInfo: AlarmMetadata {
|
|
105
|
+
var blend: String
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@available(iOS 26.1, *)
|
|
109
|
+
func startSteepTimer() async throws {
|
|
110
|
+
let presentation = AlarmPresentation(
|
|
111
|
+
alert: .init(title: "Tea is ready"),
|
|
112
|
+
countdown: .init(title: "Steeping"),
|
|
113
|
+
paused: .init(title: "Steeping paused",
|
|
114
|
+
resumeButton: AlarmButton(text: "Resume", textColor: .white, systemImageName: "play.fill"))
|
|
115
|
+
)
|
|
116
|
+
let attributes = AlarmAttributes(presentation: presentation,
|
|
117
|
+
metadata: BrewInfo(blend: "Sencha"),
|
|
118
|
+
tintColor: .green)
|
|
119
|
+
let threeMinutes = AlarmManager.AlarmConfiguration.timer(duration: 180, attributes: attributes, sound: .default)
|
|
120
|
+
let center = AlarmManager.shared
|
|
121
|
+
_ = try await center.schedule(id: UUID(), configuration: threeMinutes)
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### AlarmPresentation
|
|
126
|
+
|
|
127
|
+
`AlarmPresentation` describes the text and buttons per state. The system draws
|
|
128
|
+
the alerting UI from it; the widget extension may draw countdown and paused.
|
|
129
|
+
|
|
130
|
+
| Type | State | Required |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `AlarmPresentation.Alert(title:secondaryButton:secondaryButtonBehavior:)` | alerting | yes |
|
|
133
|
+
| `AlarmPresentation.Countdown(title:pauseButton:)` | counting down | no |
|
|
134
|
+
| `AlarmPresentation.Paused(title:resumeButton:)` | paused | no |
|
|
135
|
+
|
|
136
|
+
Combine them with `AlarmPresentation(alert:countdown:paused:)`:
|
|
137
|
+
|
|
138
|
+
```swift
|
|
139
|
+
import AlarmKit
|
|
140
|
+
import SwiftUI
|
|
141
|
+
|
|
142
|
+
@available(iOS 26.1, *)
|
|
143
|
+
func workoutPresentation() -> AlarmPresentation {
|
|
144
|
+
let again = AlarmButton(text: "Again", textColor: .white, systemImageName: "repeat")
|
|
145
|
+
let hold = AlarmButton(text: "Hold", textColor: .orange, systemImageName: "pause.fill")
|
|
146
|
+
let go = AlarmButton(text: "Go", textColor: .orange, systemImageName: "play.fill")
|
|
147
|
+
return AlarmPresentation(
|
|
148
|
+
alert: .init(title: "Rest over", secondaryButton: again, secondaryButtonBehavior: .countdown),
|
|
149
|
+
countdown: .init(title: "Resting", pauseButton: hold),
|
|
150
|
+
paused: .init(title: "Rest on hold", resumeButton: go)
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Metadata
|
|
156
|
+
|
|
157
|
+
```swift
|
|
158
|
+
import AlarmKit
|
|
159
|
+
import SwiftUI
|
|
160
|
+
|
|
161
|
+
struct PlantInfo: AlarmMetadata { var plantName: String; var litres: Double }
|
|
162
|
+
struct NoAlarmData: AlarmMetadata {}
|
|
163
|
+
|
|
164
|
+
@available(iOS 26.1, *)
|
|
165
|
+
func plantAttributes() -> (AlarmAttributes<PlantInfo>, AlarmAttributes<NoAlarmData>) {
|
|
166
|
+
let look = AlarmPresentation(alert: .init(title: "Water the fern"))
|
|
167
|
+
let withData = AlarmAttributes(presentation: look, metadata: PlantInfo(plantName: "Fern", litres: 0.5), tintColor: .mint)
|
|
168
|
+
let plain = AlarmAttributes<NoAlarmData>(presentation: look, metadata: nil, tintColor: .mint)
|
|
169
|
+
return (withData, plain)
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
When there is nothing to carry, declare an empty type that conforms to
|
|
174
|
+
`AlarmMetadata` and pass `metadata: nil`.
|
|
175
|
+
|
|
176
|
+
## Live Activity integration
|
|
177
|
+
|
|
178
|
+
A ringing alarm is a Live Activity that the system renders on the Lock Screen,
|
|
179
|
+
in the Dynamic Island, in StandBy and on a paired Apple Watch; the app has no
|
|
180
|
+
say over that screen. Countdown and paused are different: the app supplies a widget
|
|
181
|
+
extension whose `ActivityConfiguration` names the same
|
|
182
|
+
`AlarmAttributes<Metadata>` type used at scheduling.
|
|
183
|
+
|
|
184
|
+
- Plan on the extension whenever countdown presentation is used. The metadata
|
|
185
|
+
type must be compiled into both the app and the extension, and it should stay
|
|
186
|
+
small.
|
|
187
|
+
- Leave the extension out and an alarm can vanish early or never ring. The
|
|
188
|
+
system only draws a fallback countdown in narrow cases, such as after a
|
|
189
|
+
restart before the first unlock.
|
|
190
|
+
|
|
191
|
+
AlarmKit is responsible for: permission to schedule alarms, `AlarmManager` scheduling and state,
|
|
192
|
+
`AlarmAttributes`, `AlarmPresentation`, `AlarmPresentationState`, the sound,
|
|
193
|
+
and the system Stop, Repeat and Open App actions. It is not a general surface
|
|
194
|
+
for custom notification UI.
|
|
195
|
+
|
|
196
|
+
When describing where alarms appear, name Lock Screen, Dynamic Island, StandBy
|
|
197
|
+
and paired Apple Watch together. Smart Stack is not an AlarmKit surface.
|
|
198
|
+
|
|
199
|
+
Hand-offs:
|
|
200
|
+
|
|
201
|
+
- Widgets on the Home Screen or in Smart Stack, `WidgetFamily`, timelines,
|
|
202
|
+
`WidgetCenter` reloads: `widgetkit`.
|
|
203
|
+
- Non-alarm Live Activities (`Activity.request`, `update`, `end`),
|
|
204
|
+
push-to-start and per-activity update tokens, and remote `content-state`
|
|
205
|
+
payloads: `live-activities`.
|
|
206
|
+
- APNs, `UNUserNotificationCenter`, notification categories and actions,
|
|
207
|
+
custom notification UI: `push-notifications`, unless the code ends up
|
|
208
|
+
calling `AlarmManager`.
|
|
209
|
+
|
|
210
|
+
Setup stays with what Apple documents: `NSAlarmKitUsageDescription` and
|
|
211
|
+
`AlarmManager` authorization. Do not require extra keys or a
|
|
212
|
+
`com.apple.developer.alarmkit` entitlement unless a current Apple source says so.
|
|
213
|
+
|
|
214
|
+
```swift
|
|
215
|
+
import ActivityKit
|
|
216
|
+
import AlarmKit
|
|
217
|
+
import SwiftUI
|
|
218
|
+
import WidgetKit
|
|
219
|
+
|
|
220
|
+
struct CoachInfo: AlarmMetadata {
|
|
221
|
+
var drill: String
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
struct CoachAlarmWidget: Widget {
|
|
225
|
+
var body: some WidgetConfiguration {
|
|
226
|
+
ActivityConfiguration(for: AlarmAttributes<CoachInfo>.self) { context in
|
|
227
|
+
Text(context.attributes.metadata?.drill ?? "Timer")
|
|
228
|
+
.padding()
|
|
229
|
+
} dynamicIsland: { context in
|
|
230
|
+
DynamicIsland {
|
|
231
|
+
DynamicIslandExpandedRegion(.center) {
|
|
232
|
+
let heading = context.attributes.presentation.alert.title
|
|
233
|
+
Text(heading)
|
|
234
|
+
}
|
|
235
|
+
DynamicIslandExpandedRegion(.bottom) {
|
|
236
|
+
if case .countdown(let run) = context.state.mode {
|
|
237
|
+
Text(timerInterval: Date.now...run.fireDate, countsDown: true)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} compactLeading: {
|
|
241
|
+
Image(systemName: "alarm.fill")
|
|
242
|
+
} compactTrailing: {
|
|
243
|
+
Image(systemName: "alarm.fill")
|
|
244
|
+
} minimal: {
|
|
245
|
+
Image(systemName: "alarm.fill")
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@main
|
|
252
|
+
struct CoachWidgets: WidgetBundle {
|
|
253
|
+
var body: some Widget {
|
|
254
|
+
CoachAlarmWidget()
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
@@ -179,32 +179,9 @@ The data layer implements repository interfaces. It coordinates between remote
|
|
|
179
179
|
|
|
180
180
|
### Repository Implementation
|
|
181
181
|
|
|
182
|
-
|
|
183
|
-
class OrderRepositoryImpl @Inject constructor(
|
|
184
|
-
private val remoteDataSource: OrderRemoteDataSource,
|
|
185
|
-
private val localDataSource: OrderLocalDataSource,
|
|
186
|
-
private val orderMapper: OrderMapper,
|
|
187
|
-
) : OrderRepository {
|
|
188
|
-
|
|
189
|
-
override fun getOrders(store: String, region: String): Flow<List<Order>> =
|
|
190
|
-
localDataSource.getOrders(store, region)
|
|
191
|
-
.map { entities -> entities.map(orderMapper::toDomain) }
|
|
192
|
-
|
|
193
|
-
override fun searchOrders(query: String, date: LocalDate): Flow<List<Order>> = flow {
|
|
194
|
-
val dtos = remoteDataSource.searchOrders(query, date.toString())
|
|
195
|
-
localDataSource.insertOrders(dtos.map(orderMapper::toEntity))
|
|
196
|
-
emitAll(
|
|
197
|
-
localDataSource.getOrders(query, query)
|
|
198
|
-
.map { entities -> entities.map(orderMapper::toDomain) }
|
|
199
|
-
)
|
|
200
|
-
}
|
|
182
|
+
The implementation is injected, maps entities to domain models through the mapper on every read, exposes reads as `Flow`, and wraps suspend writes in `Result` with `runCatching`.
|
|
201
183
|
|
|
202
|
-
|
|
203
|
-
val response = remoteDataSource.payOrder(orderId)
|
|
204
|
-
orderMapper.toPaymentDomain(response)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
```
|
|
184
|
+
Code: [Repository Implementation](references/code-examples.md#repository-implementation).
|
|
208
185
|
|
|
209
186
|
### Data Sources
|
|
210
187
|
|
|
@@ -394,39 +371,7 @@ depending on every `impl`. Full host injection and per-feature contribution:
|
|
|
394
371
|
|
|
395
372
|
Mappers isolate layer boundaries. Each layer has its own model type.
|
|
396
373
|
|
|
397
|
-
|
|
398
|
-
class OrderMapper @Inject constructor() {
|
|
399
|
-
|
|
400
|
-
fun toDomain(entity: OrderEntity): Order = Order(
|
|
401
|
-
id = entity.id,
|
|
402
|
-
store = Location(entity.storeCode, entity.storeName),
|
|
403
|
-
region = Location(entity.regionCode, entity.regionName),
|
|
404
|
-
placedAt = Instant.parse(entity.placedAt),
|
|
405
|
-
deliveredAt = Instant.parse(entity.deliveredAt),
|
|
406
|
-
price = Price(entity.priceAmount, entity.priceCurrency),
|
|
407
|
-
status = OrderStatus.valueOf(entity.status),
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
fun toEntity(dto: OrderDto): OrderEntity = OrderEntity(
|
|
411
|
-
id = dto.id,
|
|
412
|
-
storeCode = dto.store.code,
|
|
413
|
-
storeName = dto.store.name,
|
|
414
|
-
regionCode = dto.region.code,
|
|
415
|
-
regionName = dto.region.name,
|
|
416
|
-
placedAt = dto.placedAt,
|
|
417
|
-
deliveredAt = dto.deliveredAt,
|
|
418
|
-
priceAmount = dto.price.amount,
|
|
419
|
-
priceCurrency = dto.price.currency,
|
|
420
|
-
status = dto.status,
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
fun toPaymentDomain(response: PaymentResponse): Payment = Payment(
|
|
424
|
-
id = response.paymentId,
|
|
425
|
-
confirmationCode = response.confirmationCode,
|
|
426
|
-
status = PaymentStatus.valueOf(response.status),
|
|
427
|
-
)
|
|
428
|
-
}
|
|
429
|
-
```
|
|
374
|
+
Code: [Mapper Pattern](references/code-examples.md#mapper-pattern).
|
|
430
375
|
|
|
431
376
|
## Do's and Don'ts
|
|
432
377
|
|
|
@@ -487,3 +432,7 @@ class OrderMapper @Inject constructor() {
|
|
|
487
432
|
- [ ] Feature and data internals are `internal`
|
|
488
433
|
- [ ] Coroutine dispatchers are injectable via `DispatcherProvider`
|
|
489
434
|
- [ ] No `GlobalScope` usage; all coroutines use structured concurrency
|
|
435
|
+
|
|
436
|
+
## References
|
|
437
|
+
|
|
438
|
+
- [Code examples](references/code-examples.md): open for the full code behind Repository Implementation, Mapper Pattern.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Android Architecture Code Examples
|
|
2
|
+
|
|
3
|
+
Full code samples for the SKILL.md sections that link here. The rules and the decision guidance stay in SKILL.md; each heading below matches the SKILL.md section the code belongs to.
|
|
4
|
+
|
|
5
|
+
## Repository Implementation
|
|
6
|
+
|
|
7
|
+
```kotlin
|
|
8
|
+
class OrderRepositoryImpl @Inject constructor(
|
|
9
|
+
private val remoteDataSource: OrderRemoteDataSource,
|
|
10
|
+
private val localDataSource: OrderLocalDataSource,
|
|
11
|
+
private val orderMapper: OrderMapper,
|
|
12
|
+
) : OrderRepository {
|
|
13
|
+
|
|
14
|
+
override fun getOrders(store: String, region: String): Flow<List<Order>> =
|
|
15
|
+
localDataSource.getOrders(store, region)
|
|
16
|
+
.map { entities -> entities.map(orderMapper::toDomain) }
|
|
17
|
+
|
|
18
|
+
override fun searchOrders(query: String, date: LocalDate): Flow<List<Order>> = flow {
|
|
19
|
+
val dtos = remoteDataSource.searchOrders(query, date.toString())
|
|
20
|
+
localDataSource.insertOrders(dtos.map(orderMapper::toEntity))
|
|
21
|
+
emitAll(
|
|
22
|
+
localDataSource.getOrders(query, query)
|
|
23
|
+
.map { entities -> entities.map(orderMapper::toDomain) }
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override suspend fun payOrder(orderId: String): Result<Payment> = runCatching {
|
|
28
|
+
val response = remoteDataSource.payOrder(orderId)
|
|
29
|
+
orderMapper.toPaymentDomain(response)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Mapper Pattern
|
|
35
|
+
|
|
36
|
+
```kotlin
|
|
37
|
+
class OrderMapper @Inject constructor() {
|
|
38
|
+
|
|
39
|
+
fun toDomain(entity: OrderEntity): Order = Order(
|
|
40
|
+
id = entity.id,
|
|
41
|
+
store = Location(entity.storeCode, entity.storeName),
|
|
42
|
+
region = Location(entity.regionCode, entity.regionName),
|
|
43
|
+
placedAt = Instant.parse(entity.placedAt),
|
|
44
|
+
deliveredAt = Instant.parse(entity.deliveredAt),
|
|
45
|
+
price = Price(entity.priceAmount, entity.priceCurrency),
|
|
46
|
+
status = OrderStatus.valueOf(entity.status),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
fun toEntity(dto: OrderDto): OrderEntity = OrderEntity(
|
|
50
|
+
id = dto.id,
|
|
51
|
+
storeCode = dto.store.code,
|
|
52
|
+
storeName = dto.store.name,
|
|
53
|
+
regionCode = dto.region.code,
|
|
54
|
+
regionName = dto.region.name,
|
|
55
|
+
placedAt = dto.placedAt,
|
|
56
|
+
deliveredAt = dto.deliveredAt,
|
|
57
|
+
priceAmount = dto.price.amount,
|
|
58
|
+
priceCurrency = dto.price.currency,
|
|
59
|
+
status = dto.status,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
fun toPaymentDomain(response: PaymentResponse): Payment = Payment(
|
|
63
|
+
id = response.paymentId,
|
|
64
|
+
confirmationCode = response.confirmationCode,
|
|
65
|
+
status = PaymentStatus.valueOf(response.status),
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
```
|
|
@@ -60,29 +60,7 @@ The system builds the intent with `init()`, fills in parameters, then calls
|
|
|
60
60
|
`perform()`. A static `title` (a `LocalizedStringResource`) and a
|
|
61
61
|
`parameterSummary` shape how Shortcuts shows it.
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
import AppIntents
|
|
65
|
-
|
|
66
|
-
struct WaterPlantIntent: AppIntent {
|
|
67
|
-
static let title: LocalizedStringResource = "Water Plant"
|
|
68
|
-
static let description = IntentDescription("Logs a watering for one of your plants.")
|
|
69
|
-
|
|
70
|
-
@Parameter(title: "Plant")
|
|
71
|
-
var plant: PlantEntity
|
|
72
|
-
|
|
73
|
-
@Parameter(title: "Cups", default: 1)
|
|
74
|
-
var cups: Int
|
|
75
|
-
|
|
76
|
-
static var parameterSummary: some ParameterSummary {
|
|
77
|
-
Summary("Water \(\.$plant)") { \.$cups }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
func perform() async throws -> some IntentResult & ProvidesDialog {
|
|
81
|
-
try await CareLog.shared.recordWatering(plantID: plant.id, cups: cups)
|
|
82
|
-
return .result(dialog: "Logged \(cups) cups for \(plant.name).")
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
```
|
|
63
|
+
Code: [AppIntent](references/code-examples.md#appintent).
|
|
86
64
|
|
|
87
65
|
Optional members:
|
|
88
66
|
|
|
@@ -330,28 +308,7 @@ Adopt `IndexedEntity` to make entities searchable. `indexingKey` ties a
|
|
|
330
308
|
property to a `CSSearchableItemAttributeSet` key path: on `@Property` from
|
|
331
309
|
iOS 18.4, on `@ComputedProperty` from iOS 26.
|
|
332
310
|
|
|
333
|
-
|
|
334
|
-
import CoreSpotlight
|
|
335
|
-
|
|
336
|
-
struct CareGuideEntity: IndexedEntity {
|
|
337
|
-
static let defaultQuery = CareGuideQuery()
|
|
338
|
-
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Care Guide"
|
|
339
|
-
|
|
340
|
-
let id: UUID
|
|
341
|
-
|
|
342
|
-
@Property(title: "Title", indexingKey: \.title)
|
|
343
|
-
var title: String
|
|
344
|
-
|
|
345
|
-
var steps: [String]
|
|
346
|
-
|
|
347
|
-
@ComputedProperty(indexingKey: \.contentDescription)
|
|
348
|
-
var overview: String { steps.prefix(2).joined(separator: " ") }
|
|
349
|
-
|
|
350
|
-
var displayRepresentation: DisplayRepresentation { DisplayRepresentation(title: "\(title)") }
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// try await CSSearchableIndex(name: "guides").indexAppEntities(guides)
|
|
354
|
-
```
|
|
311
|
+
Code: [Spotlight and IndexedEntity (iOS 18+)](references/code-examples.md#spotlight-and-indexedentity-ios-18).
|
|
355
312
|
|
|
356
313
|
- IDs must be stable: a UUID or slug, never a database row ID that can be
|
|
357
314
|
reused.
|
|
@@ -445,3 +402,4 @@ The [advanced guide](references/appintents-advanced.md) covers every
|
|
|
445
402
|
`@Parameter` form, `EntityPropertyQuery`, assistant schemas, Focus filters,
|
|
446
403
|
SiriKit migration, errors and dialogs, confirmation, authentication, URL
|
|
447
404
|
representations and Spotlight indexing.
|
|
405
|
+
- [Code examples](references/code-examples.md): open for the full code behind AppIntent, Spotlight and IndexedEntity (iOS 18+).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# App Intents Code Examples
|
|
2
|
+
|
|
3
|
+
Full code samples for the SKILL.md sections that link here. The rules and the decision guidance stay in SKILL.md; each heading below matches the SKILL.md section the code belongs to.
|
|
4
|
+
|
|
5
|
+
## AppIntent
|
|
6
|
+
|
|
7
|
+
```swift
|
|
8
|
+
import AppIntents
|
|
9
|
+
|
|
10
|
+
struct WaterPlantIntent: AppIntent {
|
|
11
|
+
static let title: LocalizedStringResource = "Water Plant"
|
|
12
|
+
static let description = IntentDescription("Logs a watering for one of your plants.")
|
|
13
|
+
|
|
14
|
+
@Parameter(title: "Plant")
|
|
15
|
+
var plant: PlantEntity
|
|
16
|
+
|
|
17
|
+
@Parameter(title: "Cups", default: 1)
|
|
18
|
+
var cups: Int
|
|
19
|
+
|
|
20
|
+
static var parameterSummary: some ParameterSummary {
|
|
21
|
+
Summary("Water \(\.$plant)") { \.$cups }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func perform() async throws -> some IntentResult & ProvidesDialog {
|
|
25
|
+
try await CareLog.shared.recordWatering(plantID: plant.id, cups: cups)
|
|
26
|
+
return .result(dialog: "Logged \(cups) cups for \(plant.name).")
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Spotlight and IndexedEntity (iOS 18+)
|
|
32
|
+
|
|
33
|
+
```swift
|
|
34
|
+
import CoreSpotlight
|
|
35
|
+
|
|
36
|
+
struct CareGuideEntity: IndexedEntity {
|
|
37
|
+
static let defaultQuery = CareGuideQuery()
|
|
38
|
+
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Care Guide"
|
|
39
|
+
|
|
40
|
+
let id: UUID
|
|
41
|
+
|
|
42
|
+
@Property(title: "Title", indexingKey: \.title)
|
|
43
|
+
var title: String
|
|
44
|
+
|
|
45
|
+
var steps: [String]
|
|
46
|
+
|
|
47
|
+
@ComputedProperty(indexingKey: \.contentDescription)
|
|
48
|
+
var overview: String { steps.prefix(2).joined(separator: " ") }
|
|
49
|
+
|
|
50
|
+
var displayRepresentation: DisplayRepresentation { DisplayRepresentation(title: "\(title)") }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// try await CSSearchableIndex(name: "guides").indexAppEntities(guides)
|
|
54
|
+
```
|
|
@@ -206,41 +206,7 @@ Keep the brief as text beside the frames so a new locale or size reuses it.
|
|
|
206
206
|
|
|
207
207
|
## Asking for reviews in the app
|
|
208
208
|
|
|
209
|
-
Use
|
|
210
|
-
`AppStore.requestReview(in:)`. The full StoreKit surface is covered in
|
|
211
|
-
`storekit`.
|
|
212
|
-
|
|
213
|
-
```swift
|
|
214
|
-
import StoreKit
|
|
215
|
-
import SwiftUI
|
|
216
|
-
|
|
217
|
-
struct TripSummaryView: View {
|
|
218
|
-
@Environment(\.requestReview) private var requestReview
|
|
219
|
-
let completedTrips: Int
|
|
220
|
-
|
|
221
|
-
var body: some View {
|
|
222
|
-
TripSummaryContent()
|
|
223
|
-
.onAppear {
|
|
224
|
-
if completedTrips == 5 { requestReview() }
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
- The system shows the prompt at most three times in 365 days per device for
|
|
231
|
-
someone who has not rated the app.
|
|
232
|
-
- Calling the API is a request; StoreKit decides whether anything appears.
|
|
233
|
-
- Development builds always show it; TestFlight builds never do.
|
|
234
|
-
- Good moments: a meaningful task just finished, a milestone or streak, a
|
|
235
|
-
visible success, several active sessions.
|
|
236
|
-
- Bad moments: the very first launch, the onboarding flow, any error or crash,
|
|
237
|
-
a purchase that failed, or directly after a button tap.
|
|
238
|
-
- Do not put a "Do you like this app?" dialog in front of the system prompt.
|
|
239
|
-
Apple discourages intercepting it and may reject apps that do.
|
|
240
|
-
|
|
241
|
-
For a permanent "Write a review" row in settings, open
|
|
242
|
-
`https://apps.apple.com/app/id{APP_ID}?action=write-review`. It goes straight
|
|
243
|
-
to the review sheet and does not count against the three-per-year limit.
|
|
209
|
+
Use `RequestReviewAction` (or `AppStore.requestReview(in:)`) after a meaningful success, never on first launch, onboarding, errors or a failed purchase, and never behind a pre-prompt; the system caps it at three prompts a year. Code, timing rules and the write-review link: [Reviews, events and localized metadata](references/engagement-and-localization.md).
|
|
244
210
|
|
|
245
211
|
## Custom Product Pages
|
|
246
212
|
|
|
@@ -267,24 +233,7 @@ URL format, channel mapping and maintenance are in the
|
|
|
267
233
|
|
|
268
234
|
## In-App Events
|
|
269
235
|
|
|
270
|
-
|
|
271
|
-
each user. They raise visibility while they run and can bring lapsed
|
|
272
|
-
users back.
|
|
273
|
-
|
|
274
|
-
- Badges (seven): Challenge, Competition, Live Event, Major Update, New
|
|
275
|
-
Season, Premiere, Special Event. Pick the one that matches what actually happens in
|
|
276
|
-
the app, and never create an event without real time-bound content.
|
|
277
|
-
- Text limits in characters: 30 for the name, 50 for the short description,
|
|
278
|
-
120 for the long one.
|
|
279
|
-
- Media: an event card image or video at 16:9 (1920 x 1080) and an event
|
|
280
|
-
details page image or video at 9:16 (1080 x 1920).
|
|
281
|
-
- Time them with feature releases, seasonal moments and content drops.
|
|
282
|
-
- Write the name and short description for someone scanning search results;
|
|
283
|
-
they appear on the event cards in Search and Today.
|
|
284
|
-
- The card is shown on the product page as well, and a well-timed card lifts
|
|
285
|
-
tap-through.
|
|
286
|
-
- Line events up so a new one starts as the previous one ends, keeping a card
|
|
287
|
-
in search at all times.
|
|
236
|
+
Seven badges, only for real time-bound content; name 30, short description 50, long description 120 characters; 16:9 card and 9:16 details media; keep one event live at all times. Full rules: [Reviews, events and localized metadata](references/engagement-and-localization.md).
|
|
288
237
|
|
|
289
238
|
## Product Page Optimization
|
|
290
239
|
|
|
@@ -313,37 +262,11 @@ default product page.
|
|
|
313
262
|
|
|
314
263
|
## Ratings and reviews
|
|
315
264
|
|
|
316
|
-
|
|
317
|
-
search, and they move both ranking and conversion.
|
|
318
|
-
|
|
319
|
-
- Earn more recent positive reviews: prompt after successes and answer
|
|
320
|
-
complaints constructively.
|
|
321
|
-
- Answer critical reviews from App Store Connect. A calm, specific reply
|
|
322
|
-
improves how the app looks even when the rating does not change: acknowledge
|
|
323
|
-
the problem, say what is fixed or in progress, keep the tone neutral.
|
|
324
|
-
- Read reviews as research into what users want, what frustrates them and the
|
|
325
|
-
words they use, then feed that into metadata and captions.
|
|
326
|
-
- Submitting a new version offers the option of resetting the summary rating. Do it only
|
|
327
|
-
when the shown rating is far below current quality after major fixes. Keep a
|
|
328
|
-
good rating that reflects the app fairly, skip the reset for routine fixes, and after
|
|
329
|
-
a risky redesign wait until it has settled.
|
|
265
|
+
Earn recent positive reviews, answer critical ones calmly and specifically, mine reviews for wording, and reset the summary rating only after major fixes. Details: [Reviews, events and localized metadata](references/engagement-and-localization.md).
|
|
330
266
|
|
|
331
267
|
## Localized Metadata Optimization
|
|
332
268
|
|
|
333
|
-
|
|
334
|
-
Keywords, descriptions and captions are researched and written per market.
|
|
335
|
-
|
|
336
|
-
- Research keywords in each market. The term people use to find a photo editor
|
|
337
|
-
in Japanese is not a word-for-word translation of the English one.
|
|
338
|
-
- Rewrite the description per locale, adapting opening lines, features, proof
|
|
339
|
-
and close to local expectations.
|
|
340
|
-
- Localize every surface (description, keywords, previews, screenshots), and do
|
|
341
|
-
not count on literal translations or on one locale's keywords being indexed
|
|
342
|
-
elsewhere.
|
|
343
|
-
- Caption screenshots in the market's language; native captions convert better
|
|
344
|
-
than English ones.
|
|
345
|
-
- Localizing strings inside the app (String Catalogs, `FormatStyle`,
|
|
346
|
-
right-to-left layout) is covered by `ios-localization`.
|
|
269
|
+
Research keywords and rewrite descriptions and captions per market instead of translating; in-app string localization belongs to `ios-localization`. Details: [Reviews, events and localized metadata](references/engagement-and-localization.md).
|
|
347
270
|
|
|
348
271
|
## Mistakes to catch
|
|
349
272
|
|
|
@@ -383,6 +306,7 @@ Keywords, descriptions and captions are researched and written per market.
|
|
|
383
306
|
|
|
384
307
|
## References
|
|
385
308
|
|
|
309
|
+
- [Reviews, events and localized metadata](references/engagement-and-localization.md): review prompt timing and API, In-App Event badges and limits, replying to reviews and rating resets, and per-market metadata.
|
|
386
310
|
- [Keyword research](references/keyword-research-methodology.md):
|
|
387
311
|
what search indexes, the research process, scoring, slot allocation, seasonal
|
|
388
312
|
rotation, before and after examples.
|