@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
|
@@ -43,26 +43,7 @@ let sharedDB = container.sharedCloudDatabase
|
|
|
43
43
|
A `CKRecord` is a typed dictionary. One record can hold up to 1 MB, not
|
|
44
44
|
counting the files behind any `CKAsset` fields.
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
func plant(named name: String, in db: CKDatabase) async throws -> CKRecord {
|
|
48
|
-
let plant = CKRecord(recordType: "Plant")
|
|
49
|
-
plant["name"] = name as CKRecordValue
|
|
50
|
-
plant["plantedOn"] = Date.now as CKRecordValue
|
|
51
|
-
plant["tags"] = ["herb", "sunny"] as CKRecordValue
|
|
52
|
-
return try await db.save(plant)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
func rename(_ recordName: String, to newName: String, in db: CKDatabase) async throws {
|
|
56
|
-
let id = CKRecord.ID(recordName: recordName)
|
|
57
|
-
let existing = try await db.record(for: id) // always fetch before editing
|
|
58
|
-
existing["name"] = newName as CKRecordValue
|
|
59
|
-
_ = try await db.save(existing)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
func uproot(_ recordName: String, in db: CKDatabase) async throws {
|
|
63
|
-
_ = try await db.deleteRecord(withID: CKRecord.ID(recordName: recordName))
|
|
64
|
-
}
|
|
65
|
-
```
|
|
46
|
+
Code for this section: [CloudKit code](references/cloudkit-code.md).
|
|
66
47
|
|
|
67
48
|
### Custom zones
|
|
68
49
|
|
|
@@ -120,38 +101,7 @@ With CloudKit enabled, Xcode manages the push entitlement; no separate App ID
|
|
|
120
101
|
push setup is needed. Silent pushes still require **Background Modes >
|
|
121
102
|
Remote notifications**.
|
|
122
103
|
|
|
123
|
-
|
|
124
|
-
let newPlants = CKQuerySubscription(
|
|
125
|
-
recordType: "Plant",
|
|
126
|
-
predicate: NSPredicate(value: true),
|
|
127
|
-
subscriptionID: "plant-changes",
|
|
128
|
-
options: [.firesOnRecordCreation, .firesOnRecordUpdate]
|
|
129
|
-
)
|
|
130
|
-
let info = CKSubscription.NotificationInfo()
|
|
131
|
-
info.shouldSendContentAvailable = true // silent
|
|
132
|
-
newPlants.notificationInfo = info
|
|
133
|
-
_ = try await publicDB.save(newPlants)
|
|
134
|
-
|
|
135
|
-
let everything = CKDatabaseSubscription(subscriptionID: "private-db") // any change in the DB
|
|
136
|
-
let oneZone = CKRecordZoneSubscription(zoneID: beds, subscriptionID: "beds") // changes in one zone
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
```swift
|
|
140
|
-
import UIKit
|
|
141
|
-
|
|
142
|
-
// UIApplicationDelegate
|
|
143
|
-
func application(_ application: UIApplication,
|
|
144
|
-
didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
|
|
145
|
-
guard let note = CKNotification(fromRemoteNotificationDictionary: userInfo),
|
|
146
|
-
note.subscriptionID == "private-db" else { return .noData }
|
|
147
|
-
do {
|
|
148
|
-
try await GardenSync.shared.pullNow() // CKSyncEngine or CKFetchRecordZoneChangesOperation
|
|
149
|
-
return .newData
|
|
150
|
-
} catch {
|
|
151
|
-
return .failed
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
```
|
|
104
|
+
Code for this section: [CloudKit code](references/cloudkit-code.md).
|
|
155
105
|
|
|
156
106
|
## CKSyncEngine (iOS 17+)
|
|
157
107
|
|
|
@@ -164,65 +114,14 @@ Its automatic timing is not predictable. It needs the CloudKit capability plus
|
|
|
164
114
|
Remote notifications, and works only with private and shared databases, never
|
|
165
115
|
public.
|
|
166
116
|
|
|
167
|
-
```swift
|
|
168
|
-
actor GardenSync: CKSyncEngineDelegate {
|
|
169
|
-
static let shared = GardenSync()
|
|
170
|
-
private var engine: CKSyncEngine?
|
|
171
|
-
|
|
172
|
-
func start() {
|
|
173
|
-
guard engine == nil else { return }
|
|
174
|
-
let config = CKSyncEngine.Configuration(
|
|
175
|
-
database: CKContainer.default().privateCloudDatabase,
|
|
176
|
-
stateSerialization: StateFile.load(), // CKSyncEngine.State.Serialization?
|
|
177
|
-
delegate: self
|
|
178
|
-
)
|
|
179
|
-
engine = CKSyncEngine(config)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async {
|
|
183
|
-
switch event {
|
|
184
|
-
case .stateUpdate(let update):
|
|
185
|
-
StateFile.save(update.stateSerialization) // resume from the right token next launch
|
|
186
|
-
case .accountChange(let change):
|
|
187
|
-
await LocalStore.shared.handle(change)
|
|
188
|
-
case .fetchedRecordZoneChanges(let fetched):
|
|
189
|
-
for mod in fetched.modifications { await LocalStore.shared.upsert(mod.record) }
|
|
190
|
-
for gone in fetched.deletions { await LocalStore.shared.remove(gone.recordID) }
|
|
191
|
-
case .sentRecordZoneChanges(let sent):
|
|
192
|
-
for saved in sent.savedRecords { await LocalStore.shared.markSynced(saved) }
|
|
193
|
-
for failure in sent.failedRecordSaves { await resolve(failure, engine: syncEngine) }
|
|
194
|
-
default:
|
|
195
|
-
break
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
func nextRecordZoneChangeBatch(_ context: CKSyncEngine.SendChangesContext,
|
|
200
|
-
syncEngine: CKSyncEngine) async -> CKSyncEngine.RecordZoneChangeBatch? {
|
|
201
|
-
let scope = context.options.scope
|
|
202
|
-
let pending = syncEngine.state.pendingRecordZoneChanges.filter { scope.contains($0) }
|
|
203
|
-
return await CKSyncEngine.RecordZoneChangeBatch(pendingChanges: pending) { id in
|
|
204
|
-
await LocalStore.shared.record(for: id)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
func queueUpload(of id: CKRecord.ID) {
|
|
209
|
-
engine?.state.add(pendingRecordZoneChanges: [.saveRecord(id)])
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
func pullNow() async throws {
|
|
213
|
-
guard let engine else { return }
|
|
214
|
-
try await engine.fetchChanges() // pull-to-refresh or push arrival
|
|
215
|
-
try await engine.sendChanges()
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
117
|
`context.options.scope` says which zones (for example `.zoneIDs([...])`) or
|
|
221
118
|
records this send covers; filter the pending changes to it rather than
|
|
222
119
|
sending everything. Both delegate methods are `async`. Persist
|
|
223
120
|
`stateSerialization` on every `.stateUpdate`; without it the engine starts
|
|
224
121
|
over on the next launch.
|
|
225
122
|
|
|
123
|
+
Code for this section: [CloudKit code](references/cloudkit-code.md).
|
|
124
|
+
|
|
226
125
|
## SwiftData with CloudKit
|
|
227
126
|
|
|
228
127
|
`ModelConfiguration` can sync a SwiftData store to CloudKit. Any
|
|
@@ -317,28 +216,6 @@ coordinated read of a ubiquitous item waits for its contents to download
|
|
|
317
216
|
unless `.immediatelyAvailableMetadataOnly` is passed. Coordination blocks the
|
|
318
217
|
calling thread, so keep it off the main actor.
|
|
319
218
|
|
|
320
|
-
```swift
|
|
321
|
-
nonisolated func readCoordinated(_ url: URL) throws -> Data {
|
|
322
|
-
var coordinationError: NSError?
|
|
323
|
-
var result: Result<Data, any Error> = .failure(CocoaError(.fileReadUnknown))
|
|
324
|
-
NSFileCoordinator().coordinate(readingItemAt: url, options: [], error: &coordinationError) { safeURL in
|
|
325
|
-
result = Result { try Data(contentsOf: safeURL) }
|
|
326
|
-
}
|
|
327
|
-
if let coordinationError { throw coordinationError }
|
|
328
|
-
return try result.get()
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
nonisolated func writeCoordinated(_ data: Data, to url: URL) throws {
|
|
332
|
-
var coordinationError: NSError?
|
|
333
|
-
var writeError: (any Error)?
|
|
334
|
-
NSFileCoordinator().coordinate(writingItemAt: url, options: .forReplacing, error: &coordinationError) { safeURL in
|
|
335
|
-
do { try data.write(to: safeURL, options: .atomic) } catch { writeError = error }
|
|
336
|
-
}
|
|
337
|
-
if let coordinationError { throw coordinationError }
|
|
338
|
-
if let writeError { throw writeError }
|
|
339
|
-
}
|
|
340
|
-
```
|
|
341
|
-
|
|
342
219
|
An object that keeps a file open (an editor, a live preview) adopts
|
|
343
220
|
`NSFilePresenter` so it hears about changes, moves and deletions that iCloud
|
|
344
221
|
makes: implement `presentedItemURL`, return a private `OperationQueue` from
|
|
@@ -349,6 +226,8 @@ with `removeFilePresenter(_:)`. Create the presenter's own coordinators with
|
|
|
349
226
|
`UIDocument` already does all of this, so a document-based app gets
|
|
350
227
|
coordination by subclassing it.
|
|
351
228
|
|
|
229
|
+
Code for this section: [CloudKit code](references/cloudkit-code.md).
|
|
230
|
+
|
|
352
231
|
## Account status
|
|
353
232
|
|
|
354
233
|
Check the account before any sync, and listen for `.CKAccountChanged`.
|
|
@@ -389,25 +268,7 @@ A `.serverRecordChanged` error carries `ancestorRecord`, `clientRecord` and
|
|
|
389
268
|
`serverRecord`. Always merge **into `serverRecord`**, because it holds the
|
|
390
269
|
current change tag; saving the client copy just fails again.
|
|
391
270
|
|
|
392
|
-
|
|
393
|
-
func merged(_ error: CKError) -> CKRecord? {
|
|
394
|
-
guard let server = error.serverRecord, let mine = error.clientRecord else { return nil }
|
|
395
|
-
let base = error.ancestorRecord
|
|
396
|
-
for key in mine.changedKeys() {
|
|
397
|
-
let serverValue = server[key] as? NSObject
|
|
398
|
-
let baseValue = base?[key] as? NSObject
|
|
399
|
-
let myValue = mine[key] as? NSObject
|
|
400
|
-
if serverValue == baseValue {
|
|
401
|
-
server[key] = mine[key] // only I changed it
|
|
402
|
-
} else if myValue == baseValue {
|
|
403
|
-
continue // only the server changed it
|
|
404
|
-
} else {
|
|
405
|
-
server[key] = resolveBothChanged(key, mine: mine[key], theirs: server[key])
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
return server // save this one
|
|
409
|
-
}
|
|
410
|
-
```
|
|
271
|
+
Code for this section: [CloudKit code](references/cloudkit-code.md).
|
|
411
272
|
|
|
412
273
|
## Common mistakes
|
|
413
274
|
|
|
@@ -441,6 +302,7 @@ func merged(_ error: CKError) -> CKRecord? {
|
|
|
441
302
|
|
|
442
303
|
## References
|
|
443
304
|
|
|
305
|
+
- [CloudKit code](references/cloudkit-code.md): open for the code behind records, subscriptions, CKSyncEngine, coordinated file access and conflict resolution.
|
|
444
306
|
- [CloudKit patterns](references/cloudkit-patterns.md) - incremental fetches, tokens, sharing, zones, assets, batches, QoS, encryption, Dashboard
|
|
445
307
|
- [CloudKit](https://developer.apple.com/documentation/cloudkit)
|
|
446
308
|
- [CKContainer](https://developer.apple.com/documentation/cloudkit/ckcontainer)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# CloudKit code
|
|
2
|
+
|
|
3
|
+
<!-- toc -->
|
|
4
|
+
- [Records](#records)
|
|
5
|
+
- [Subscriptions](#subscriptions)
|
|
6
|
+
- [CKSyncEngine (iOS 17+)](#cksyncengine-ios-17)
|
|
7
|
+
- [Conflict resolution](#conflict-resolution)
|
|
8
|
+
<!-- /toc -->
|
|
9
|
+
|
|
10
|
+
Code for records, subscriptions, CKSyncEngine, coordinated document access and conflict resolution. SKILL.md keeps the rules and decision guidance.
|
|
11
|
+
|
|
12
|
+
## Records
|
|
13
|
+
|
|
14
|
+
A `CKRecord` is a typed dictionary. One record can hold up to 1 MB, not
|
|
15
|
+
counting the files behind any `CKAsset` fields.
|
|
16
|
+
|
|
17
|
+
```swift
|
|
18
|
+
func plant(named name: String, in db: CKDatabase) async throws -> CKRecord {
|
|
19
|
+
let plant = CKRecord(recordType: "Plant")
|
|
20
|
+
plant["name"] = name as CKRecordValue
|
|
21
|
+
plant["plantedOn"] = Date.now as CKRecordValue
|
|
22
|
+
plant["tags"] = ["herb", "sunny"] as CKRecordValue
|
|
23
|
+
return try await db.save(plant)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func rename(_ recordName: String, to newName: String, in db: CKDatabase) async throws {
|
|
27
|
+
let id = CKRecord.ID(recordName: recordName)
|
|
28
|
+
let existing = try await db.record(for: id) // always fetch before editing
|
|
29
|
+
existing["name"] = newName as CKRecordValue
|
|
30
|
+
_ = try await db.save(existing)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func uproot(_ recordName: String, in db: CKDatabase) async throws {
|
|
34
|
+
_ = try await db.deleteRecord(withID: CKRecord.ID(recordName: recordName))
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Subscriptions
|
|
39
|
+
|
|
40
|
+
A subscription makes CloudKit push a notification when server data changes.
|
|
41
|
+
With CloudKit enabled, Xcode manages the push entitlement; no separate App ID
|
|
42
|
+
push setup is needed. Silent pushes still require **Background Modes >
|
|
43
|
+
Remote notifications**.
|
|
44
|
+
|
|
45
|
+
```swift
|
|
46
|
+
let newPlants = CKQuerySubscription(
|
|
47
|
+
recordType: "Plant",
|
|
48
|
+
predicate: NSPredicate(value: true),
|
|
49
|
+
subscriptionID: "plant-changes",
|
|
50
|
+
options: [.firesOnRecordCreation, .firesOnRecordUpdate]
|
|
51
|
+
)
|
|
52
|
+
let info = CKSubscription.NotificationInfo()
|
|
53
|
+
info.shouldSendContentAvailable = true // silent
|
|
54
|
+
newPlants.notificationInfo = info
|
|
55
|
+
_ = try await publicDB.save(newPlants)
|
|
56
|
+
|
|
57
|
+
let everything = CKDatabaseSubscription(subscriptionID: "private-db") // any change in the DB
|
|
58
|
+
let oneZone = CKRecordZoneSubscription(zoneID: beds, subscriptionID: "beds") // changes in one zone
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```swift
|
|
62
|
+
import UIKit
|
|
63
|
+
|
|
64
|
+
// UIApplicationDelegate
|
|
65
|
+
func application(_ application: UIApplication,
|
|
66
|
+
didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
|
|
67
|
+
guard let note = CKNotification(fromRemoteNotificationDictionary: userInfo),
|
|
68
|
+
note.subscriptionID == "private-db" else { return .noData }
|
|
69
|
+
do {
|
|
70
|
+
try await GardenSync.shared.pullNow() // CKSyncEngine or CKFetchRecordZoneChangesOperation
|
|
71
|
+
return .newData
|
|
72
|
+
} catch {
|
|
73
|
+
return .failed
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## CKSyncEngine (iOS 17+)
|
|
79
|
+
|
|
80
|
+
For custom model data, `CKSyncEngine` is the recommended route. It schedules
|
|
81
|
+
work, retries transient failures, keeps change tokens and manages the
|
|
82
|
+
database subscription for you. It does **not** resolve app-level save
|
|
83
|
+
failures: a `CKError.serverRecordChanged` inside
|
|
84
|
+
`sentRecordZoneChanges.failedRecordSaves` needs your merge and a reschedule.
|
|
85
|
+
Its automatic timing is not predictable. It needs the CloudKit capability plus
|
|
86
|
+
Remote notifications, and works only with private and shared databases, never
|
|
87
|
+
public.
|
|
88
|
+
|
|
89
|
+
```swift
|
|
90
|
+
actor GardenSync: CKSyncEngineDelegate {
|
|
91
|
+
static let shared = GardenSync()
|
|
92
|
+
private var engine: CKSyncEngine?
|
|
93
|
+
|
|
94
|
+
func start() {
|
|
95
|
+
guard engine == nil else { return }
|
|
96
|
+
let config = CKSyncEngine.Configuration(
|
|
97
|
+
database: CKContainer.default().privateCloudDatabase,
|
|
98
|
+
stateSerialization: StateFile.load(), // CKSyncEngine.State.Serialization?
|
|
99
|
+
delegate: self
|
|
100
|
+
)
|
|
101
|
+
engine = CKSyncEngine(config)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async {
|
|
105
|
+
switch event {
|
|
106
|
+
case .stateUpdate(let update):
|
|
107
|
+
StateFile.save(update.stateSerialization) // resume from the right token next launch
|
|
108
|
+
case .accountChange(let change):
|
|
109
|
+
await LocalStore.shared.handle(change)
|
|
110
|
+
case .fetchedRecordZoneChanges(let fetched):
|
|
111
|
+
for mod in fetched.modifications { await LocalStore.shared.upsert(mod.record) }
|
|
112
|
+
for gone in fetched.deletions { await LocalStore.shared.remove(gone.recordID) }
|
|
113
|
+
case .sentRecordZoneChanges(let sent):
|
|
114
|
+
for saved in sent.savedRecords { await LocalStore.shared.markSynced(saved) }
|
|
115
|
+
for failure in sent.failedRecordSaves { await resolve(failure, engine: syncEngine) }
|
|
116
|
+
default:
|
|
117
|
+
break
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func nextRecordZoneChangeBatch(_ context: CKSyncEngine.SendChangesContext,
|
|
122
|
+
syncEngine: CKSyncEngine) async -> CKSyncEngine.RecordZoneChangeBatch? {
|
|
123
|
+
let scope = context.options.scope
|
|
124
|
+
let pending = syncEngine.state.pendingRecordZoneChanges.filter { scope.contains($0) }
|
|
125
|
+
return await CKSyncEngine.RecordZoneChangeBatch(pendingChanges: pending) { id in
|
|
126
|
+
await LocalStore.shared.record(for: id)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func queueUpload(of id: CKRecord.ID) {
|
|
131
|
+
engine?.state.add(pendingRecordZoneChanges: [.saveRecord(id)])
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func pullNow() async throws {
|
|
135
|
+
guard let engine else { return }
|
|
136
|
+
try await engine.fetchChanges() // pull-to-refresh or push arrival
|
|
137
|
+
try await engine.sendChanges()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`context.options.scope` says which zones (for example `.zoneIDs([...])`) or
|
|
143
|
+
records this send covers; filter the pending changes to it rather than
|
|
144
|
+
sending everything. Both delegate methods are `async`. Persist
|
|
145
|
+
`stateSerialization` on every `.stateUpdate`; without it the engine starts
|
|
146
|
+
over on the next launch.
|
|
147
|
+
|
|
148
|
+
### Coordinated reads and writes
|
|
149
|
+
|
|
150
|
+
The iCloud daemon reads and writes files in the ubiquity container at the same
|
|
151
|
+
time as the app, so every direct access to a ubiquitous file goes through
|
|
152
|
+
`NSFileCoordinator`. Do the file work only inside the accessor block and only
|
|
153
|
+
with the URL it passes in, which can differ from the one requested. A
|
|
154
|
+
coordinated read of a ubiquitous item waits for its contents to download
|
|
155
|
+
unless `.immediatelyAvailableMetadataOnly` is passed. Coordination blocks the
|
|
156
|
+
calling thread, so keep it off the main actor.
|
|
157
|
+
|
|
158
|
+
```swift
|
|
159
|
+
nonisolated func readCoordinated(_ url: URL) throws -> Data {
|
|
160
|
+
var coordinationError: NSError?
|
|
161
|
+
var result: Result<Data, any Error> = .failure(CocoaError(.fileReadUnknown))
|
|
162
|
+
NSFileCoordinator().coordinate(readingItemAt: url, options: [], error: &coordinationError) { safeURL in
|
|
163
|
+
result = Result { try Data(contentsOf: safeURL) }
|
|
164
|
+
}
|
|
165
|
+
if let coordinationError { throw coordinationError }
|
|
166
|
+
return try result.get()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
nonisolated func writeCoordinated(_ data: Data, to url: URL) throws {
|
|
170
|
+
var coordinationError: NSError?
|
|
171
|
+
var writeError: (any Error)?
|
|
172
|
+
NSFileCoordinator().coordinate(writingItemAt: url, options: .forReplacing, error: &coordinationError) { safeURL in
|
|
173
|
+
do { try data.write(to: safeURL, options: .atomic) } catch { writeError = error }
|
|
174
|
+
}
|
|
175
|
+
if let coordinationError { throw coordinationError }
|
|
176
|
+
if let writeError { throw writeError }
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
An object that keeps a file open (an editor, a live preview) adopts
|
|
181
|
+
`NSFilePresenter` so it hears about changes, moves and deletions that iCloud
|
|
182
|
+
makes: implement `presentedItemURL`, return a private `OperationQueue` from
|
|
183
|
+
`presentedItemOperationQueue` (the main queue invites deadlocks), react in
|
|
184
|
+
`presentedItemDidChange()`, and pair `NSFileCoordinator.addFilePresenter(_:)`
|
|
185
|
+
with `removeFilePresenter(_:)`. Create the presenter's own coordinators with
|
|
186
|
+
`NSFileCoordinator(filePresenter:)` so it is not told about its own writes.
|
|
187
|
+
`UIDocument` already does all of this, so a document-based app gets
|
|
188
|
+
coordination by subclassing it.
|
|
189
|
+
|
|
190
|
+
## Conflict resolution
|
|
191
|
+
|
|
192
|
+
A `.serverRecordChanged` error carries `ancestorRecord`, `clientRecord` and
|
|
193
|
+
`serverRecord`. Always merge **into `serverRecord`**, because it holds the
|
|
194
|
+
current change tag; saving the client copy just fails again.
|
|
195
|
+
|
|
196
|
+
```swift
|
|
197
|
+
func merged(_ error: CKError) -> CKRecord? {
|
|
198
|
+
guard let server = error.serverRecord, let mine = error.clientRecord else { return nil }
|
|
199
|
+
let base = error.ancestorRecord
|
|
200
|
+
for key in mine.changedKeys() {
|
|
201
|
+
let serverValue = server[key] as? NSObject
|
|
202
|
+
let baseValue = base?[key] as? NSObject
|
|
203
|
+
let myValue = mine[key] as? NSObject
|
|
204
|
+
if serverValue == baseValue {
|
|
205
|
+
server[key] = mine[key] // only I changed it
|
|
206
|
+
} else if myValue == baseValue {
|
|
207
|
+
continue // only the server changed it
|
|
208
|
+
} else {
|
|
209
|
+
server[key] = resolveBothChanged(key, mine: mine[key], theirs: server[key])
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return server // save this one
|
|
213
|
+
}
|
|
214
|
+
```
|
|
@@ -35,43 +35,7 @@ Material 3 (androidx.compose.material3).
|
|
|
35
35
|
Scaffold provides the standard Material 3 layout structure with slots for
|
|
36
36
|
top bar, bottom bar, FAB, snackbar, and content.
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
@OptIn(ExperimentalMaterial3Api::class)
|
|
40
|
-
@Composable
|
|
41
|
-
fun HomeScreen(
|
|
42
|
-
onNavigateToSettings: () -> Unit,
|
|
43
|
-
viewModel: HomeViewModel = hiltViewModel(),
|
|
44
|
-
) {
|
|
45
|
-
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
|
46
|
-
val snackbarHostState = remember { SnackbarHostState() }
|
|
47
|
-
|
|
48
|
-
Scaffold(
|
|
49
|
-
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
|
50
|
-
topBar = {
|
|
51
|
-
TopAppBar(
|
|
52
|
-
title = { Text("Home") },
|
|
53
|
-
actions = {
|
|
54
|
-
IconButton(onClick = onNavigateToSettings) {
|
|
55
|
-
Icon(Icons.Default.Settings, contentDescription = "Settings")
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
scrollBehavior = scrollBehavior,
|
|
59
|
-
)
|
|
60
|
-
},
|
|
61
|
-
snackbarHost = { SnackbarHost(snackbarHostState) },
|
|
62
|
-
floatingActionButton = {
|
|
63
|
-
FloatingActionButton(onClick = { /* action */ }) {
|
|
64
|
-
Icon(Icons.Default.Add, contentDescription = "Add")
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
) { innerPadding ->
|
|
68
|
-
HomeContent(
|
|
69
|
-
modifier = Modifier.padding(innerPadding),
|
|
70
|
-
viewModel = viewModel,
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
```
|
|
38
|
+
Code: [Scaffold and TopAppBar](references/code-examples.md#scaffold-and-topappbar).
|
|
75
39
|
|
|
76
40
|
### TopAppBar Variants
|
|
77
41
|
|
|
@@ -171,49 +135,7 @@ fun ScreenWithSnackbar() {
|
|
|
171
135
|
Design custom composables with slot parameters (lambdas returning `@Composable`
|
|
172
136
|
content). This follows the Material 3 pattern used by `Scaffold`, `Card`, etc.
|
|
173
137
|
|
|
174
|
-
|
|
175
|
-
@Composable
|
|
176
|
-
fun InfoCard(
|
|
177
|
-
modifier: Modifier = Modifier,
|
|
178
|
-
icon: @Composable () -> Unit,
|
|
179
|
-
title: @Composable () -> Unit,
|
|
180
|
-
subtitle: @Composable (() -> Unit)? = null,
|
|
181
|
-
actions: @Composable RowScope.() -> Unit = {},
|
|
182
|
-
) {
|
|
183
|
-
Card(modifier = modifier) {
|
|
184
|
-
Row(
|
|
185
|
-
modifier = Modifier.padding(16.dp),
|
|
186
|
-
verticalAlignment = Alignment.CenterVertically,
|
|
187
|
-
) {
|
|
188
|
-
icon()
|
|
189
|
-
Spacer(modifier = Modifier.width(16.dp))
|
|
190
|
-
Column(modifier = Modifier.weight(1f)) {
|
|
191
|
-
ProvideTextStyle(MaterialTheme.typography.titleMedium) {
|
|
192
|
-
title()
|
|
193
|
-
}
|
|
194
|
-
subtitle?.let {
|
|
195
|
-
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
|
196
|
-
it()
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
Row(content = actions)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Usage
|
|
206
|
-
InfoCard(
|
|
207
|
-
icon = { Icon(Icons.Default.ShoppingCart, contentDescription = null) },
|
|
208
|
-
title = { Text("London to New York") },
|
|
209
|
-
subtitle = { Text("Order AB1234") },
|
|
210
|
-
actions = {
|
|
211
|
-
IconButton(onClick = { /* bookmark */ }) {
|
|
212
|
-
Icon(Icons.Default.BookmarkBorder, contentDescription = "Bookmark")
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
)
|
|
216
|
-
```
|
|
138
|
+
Code: [Slot-Based API Pattern](references/code-examples.md#slot-based-api-pattern).
|
|
217
139
|
|
|
218
140
|
### Slot Design Rules
|
|
219
141
|
- First parameter: `modifier: Modifier = Modifier`
|
|
@@ -489,3 +411,7 @@ Box(
|
|
|
489
411
|
- [ ] Rich tokens exposed via `CompositionLocalProvider` and read through a `@ReadOnlyComposable` `Theme.colors`/`Theme.typography` accessor
|
|
490
412
|
- [ ] M3 `light`/`darkColorScheme` derived from the tokens so stock Material components inherit brand colors
|
|
491
413
|
- [ ] Catalog KSP processor gated behind a build flag, annotation kept universal
|
|
414
|
+
|
|
415
|
+
## References
|
|
416
|
+
|
|
417
|
+
- [Code examples](references/code-examples.md): open for the full code behind Scaffold and TopAppBar, Slot-Based API Pattern.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Compose Components 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
|
+
## Scaffold and TopAppBar
|
|
6
|
+
|
|
7
|
+
```kotlin
|
|
8
|
+
@OptIn(ExperimentalMaterial3Api::class)
|
|
9
|
+
@Composable
|
|
10
|
+
fun HomeScreen(
|
|
11
|
+
onNavigateToSettings: () -> Unit,
|
|
12
|
+
viewModel: HomeViewModel = hiltViewModel(),
|
|
13
|
+
) {
|
|
14
|
+
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
|
15
|
+
val snackbarHostState = remember { SnackbarHostState() }
|
|
16
|
+
|
|
17
|
+
Scaffold(
|
|
18
|
+
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
|
19
|
+
topBar = {
|
|
20
|
+
TopAppBar(
|
|
21
|
+
title = { Text("Home") },
|
|
22
|
+
actions = {
|
|
23
|
+
IconButton(onClick = onNavigateToSettings) {
|
|
24
|
+
Icon(Icons.Default.Settings, contentDescription = "Settings")
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
scrollBehavior = scrollBehavior,
|
|
28
|
+
)
|
|
29
|
+
},
|
|
30
|
+
snackbarHost = { SnackbarHost(snackbarHostState) },
|
|
31
|
+
floatingActionButton = {
|
|
32
|
+
FloatingActionButton(onClick = { /* action */ }) {
|
|
33
|
+
Icon(Icons.Default.Add, contentDescription = "Add")
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
) { innerPadding ->
|
|
37
|
+
HomeContent(
|
|
38
|
+
modifier = Modifier.padding(innerPadding),
|
|
39
|
+
viewModel = viewModel,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Slot-Based API Pattern
|
|
46
|
+
|
|
47
|
+
```kotlin
|
|
48
|
+
@Composable
|
|
49
|
+
fun InfoCard(
|
|
50
|
+
modifier: Modifier = Modifier,
|
|
51
|
+
icon: @Composable () -> Unit,
|
|
52
|
+
title: @Composable () -> Unit,
|
|
53
|
+
subtitle: @Composable (() -> Unit)? = null,
|
|
54
|
+
actions: @Composable RowScope.() -> Unit = {},
|
|
55
|
+
) {
|
|
56
|
+
Card(modifier = modifier) {
|
|
57
|
+
Row(
|
|
58
|
+
modifier = Modifier.padding(16.dp),
|
|
59
|
+
verticalAlignment = Alignment.CenterVertically,
|
|
60
|
+
) {
|
|
61
|
+
icon()
|
|
62
|
+
Spacer(modifier = Modifier.width(16.dp))
|
|
63
|
+
Column(modifier = Modifier.weight(1f)) {
|
|
64
|
+
ProvideTextStyle(MaterialTheme.typography.titleMedium) {
|
|
65
|
+
title()
|
|
66
|
+
}
|
|
67
|
+
subtitle?.let {
|
|
68
|
+
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
|
69
|
+
it()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
Row(content = actions)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Usage
|
|
79
|
+
InfoCard(
|
|
80
|
+
icon = { Icon(Icons.Default.ShoppingCart, contentDescription = null) },
|
|
81
|
+
title = { Text("London to New York") },
|
|
82
|
+
subtitle = { Text("Order AB1234") },
|
|
83
|
+
actions = {
|
|
84
|
+
IconButton(onClick = { /* bookmark */ }) {
|
|
85
|
+
Icon(Icons.Default.BookmarkBorder, contentDescription = "Bookmark")
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
```
|
|
@@ -293,70 +293,7 @@ NavHost(
|
|
|
293
293
|
Integrate `NavigationBar` (Material 3) with NavHost. Each tab maintains its
|
|
294
294
|
own back stack.
|
|
295
295
|
|
|
296
|
-
|
|
297
|
-
@Serializable
|
|
298
|
-
object HomeRoute
|
|
299
|
-
|
|
300
|
-
@Serializable
|
|
301
|
-
object SearchRoute
|
|
302
|
-
|
|
303
|
-
@Serializable
|
|
304
|
-
object ProfileRoute
|
|
305
|
-
|
|
306
|
-
enum class TopLevelDestination(
|
|
307
|
-
val label: String,
|
|
308
|
-
val icon: ImageVector,
|
|
309
|
-
val route: Any,
|
|
310
|
-
) {
|
|
311
|
-
HOME("Home", Icons.Default.Home, HomeRoute),
|
|
312
|
-
SEARCH("Search", Icons.Default.Search, SearchRoute),
|
|
313
|
-
PROFILE("Profile", Icons.Default.Person, ProfileRoute),
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
@Composable
|
|
317
|
-
fun MainScreen() {
|
|
318
|
-
val navController = rememberNavController()
|
|
319
|
-
val currentBackStackEntry by navController.currentBackStackEntryAsState()
|
|
320
|
-
|
|
321
|
-
Scaffold(
|
|
322
|
-
bottomBar = {
|
|
323
|
-
NavigationBar {
|
|
324
|
-
TopLevelDestination.entries.forEach { destination ->
|
|
325
|
-
val isSelected = currentBackStackEntry
|
|
326
|
-
?.destination?.hasRoute(destination.route::class) == true
|
|
327
|
-
|
|
328
|
-
NavigationBarItem(
|
|
329
|
-
selected = isSelected,
|
|
330
|
-
onClick = {
|
|
331
|
-
navController.navigate(destination.route) {
|
|
332
|
-
popUpTo(navController.graph.findStartDestination().id) {
|
|
333
|
-
saveState = true
|
|
334
|
-
}
|
|
335
|
-
launchSingleTop = true
|
|
336
|
-
restoreState = true
|
|
337
|
-
}
|
|
338
|
-
},
|
|
339
|
-
icon = {
|
|
340
|
-
Icon(destination.icon, contentDescription = destination.label)
|
|
341
|
-
},
|
|
342
|
-
label = { Text(destination.label) },
|
|
343
|
-
)
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
},
|
|
347
|
-
) { innerPadding ->
|
|
348
|
-
NavHost(
|
|
349
|
-
navController = navController,
|
|
350
|
-
startDestination = HomeRoute,
|
|
351
|
-
modifier = Modifier.padding(innerPadding),
|
|
352
|
-
) {
|
|
353
|
-
composable<HomeRoute> { HomeScreen() }
|
|
354
|
-
composable<SearchRoute> { SearchScreen() }
|
|
355
|
-
composable<ProfileRoute> { ProfileScreen() }
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
```
|
|
296
|
+
Code: [Bottom Navigation](references/code-examples.md#bottom-navigation).
|
|
360
297
|
|
|
361
298
|
Key points for bottom nav:
|
|
362
299
|
- Use `saveState = true` and `restoreState = true` to preserve tab back stacks
|
|
@@ -498,3 +435,7 @@ fun EditScreen(
|
|
|
498
435
|
- [ ] Predictive back is enabled in manifest for Android 14+
|
|
499
436
|
- [ ] Nested graphs use `navigation<>()` with explicit start destination
|
|
500
437
|
- [ ] No navigation calls inside composable body without guard
|
|
438
|
+
|
|
439
|
+
## References
|
|
440
|
+
|
|
441
|
+
- [Code examples](references/code-examples.md): open for the full code behind Bottom Navigation.
|