@mmerterden/multi-agent-pipeline 20.35.0 → 20.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +6 -5
- package/README.tr.md +6 -5
- package/docs/architecture.md +3 -3
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +4 -4
- package/install/catalog-history.json +1 -1
- package/manifest.json +210 -136
- package/package.json +5 -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/office-pair/SKILL.md +66 -0
- package/pipeline/commands/multi-agent/office-status/SKILL.md +1 -1
- 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 +38 -167
- 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/lib/qr-code.mjs +26 -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/cross-cli-contract.md +4 -4
- package/pipeline/multi-agent-refs/design-check/export.md +1 -0
- package/pipeline/multi-agent-refs/design-check/init-inventory.md +1 -1
- package/pipeline/multi-agent-refs/design-check/scope-and-ledger.md +64 -0
- package/pipeline/multi-agent-refs/features/analysis-command-notes.md +53 -0
- package/pipeline/multi-agent-refs/garbage-collect/phases.md +60 -0
- package/pipeline/multi-agent-refs/help/en.md +1 -0
- package/pipeline/multi-agent-refs/help/tr.md +1 -0
- package/pipeline/multi-agent-refs/ios-coding-standard/plan.md +71 -0
- package/pipeline/multi-agent-refs/ios-coding-standard/resolve-and-scan.md +135 -0
- package/pipeline/multi-agent-refs/jira/picker-detail.md +71 -0
- package/pipeline/multi-agent-refs/orchestrator/copilot-pipeline.md +88 -0
- package/pipeline/multi-agent-refs/orchestrator/dispatcher.md +96 -1
- package/pipeline/multi-agent-refs/refactor/sweep.md +190 -0
- package/pipeline/multi-agent-refs/resume/details.md +115 -2
- package/pipeline/multi-agent-refs/review/inputs.md +33 -1
- package/pipeline/multi-agent-refs/review/module-guides.md +9 -1
- package/pipeline/multi-agent-refs/review/panel.md +75 -0
- package/pipeline/multi-agent-refs/review/valven.md +5 -1
- package/pipeline/multi-agent-refs/setup/hooks-and-plugins.md +4 -0
- package/pipeline/multi-agent-refs/setup/token-save-flow.md +13 -0
- package/pipeline/multi-agent-refs/stack/background.md +72 -0
- package/pipeline/multi-agent-refs/store-ready/gates.md +139 -0
- package/pipeline/multi-agent-refs/store-ready/report.md +40 -2
- package/pipeline/multi-agent-refs/sync/office.md +10 -0
- package/pipeline/multi-agent-refs/sync/overview.md +61 -0
- package/pipeline/multi-agent-refs/sync/repo.md +91 -0
- package/pipeline/multi-agent-refs/update/details.md +83 -0
- package/pipeline/schemas/runs-index.schema.json +2 -1
- package/pipeline/scripts/contract-server.mjs +1 -1
- package/pipeline/scripts/office-pair.mjs +40 -9
- package/pipeline/scripts/runs-index.mjs +34 -7
- package/pipeline/scripts/status-overview.mjs +401 -0
- package/pipeline/skills/.skill-manifest.json +69 -65
- package/pipeline/skills/.skills-index.json +15 -4
- package/pipeline/skills/shared/README.md +4 -3
- package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +8 -71
- 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 +11 -73
- 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-office-pair/SKILL.md +19 -0
- package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +1 -1
- 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 +75 -219
- 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/skills-index.md +2 -1
- /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,70 @@
|
|
|
1
|
+
# Compose Navigation 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
|
+
## Bottom Navigation
|
|
6
|
+
|
|
7
|
+
```kotlin
|
|
8
|
+
@Serializable
|
|
9
|
+
object HomeRoute
|
|
10
|
+
|
|
11
|
+
@Serializable
|
|
12
|
+
object SearchRoute
|
|
13
|
+
|
|
14
|
+
@Serializable
|
|
15
|
+
object ProfileRoute
|
|
16
|
+
|
|
17
|
+
enum class TopLevelDestination(
|
|
18
|
+
val label: String,
|
|
19
|
+
val icon: ImageVector,
|
|
20
|
+
val route: Any,
|
|
21
|
+
) {
|
|
22
|
+
HOME("Home", Icons.Default.Home, HomeRoute),
|
|
23
|
+
SEARCH("Search", Icons.Default.Search, SearchRoute),
|
|
24
|
+
PROFILE("Profile", Icons.Default.Person, ProfileRoute),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Composable
|
|
28
|
+
fun MainScreen() {
|
|
29
|
+
val navController = rememberNavController()
|
|
30
|
+
val currentBackStackEntry by navController.currentBackStackEntryAsState()
|
|
31
|
+
|
|
32
|
+
Scaffold(
|
|
33
|
+
bottomBar = {
|
|
34
|
+
NavigationBar {
|
|
35
|
+
TopLevelDestination.entries.forEach { destination ->
|
|
36
|
+
val isSelected = currentBackStackEntry
|
|
37
|
+
?.destination?.hasRoute(destination.route::class) == true
|
|
38
|
+
|
|
39
|
+
NavigationBarItem(
|
|
40
|
+
selected = isSelected,
|
|
41
|
+
onClick = {
|
|
42
|
+
navController.navigate(destination.route) {
|
|
43
|
+
popUpTo(navController.graph.findStartDestination().id) {
|
|
44
|
+
saveState = true
|
|
45
|
+
}
|
|
46
|
+
launchSingleTop = true
|
|
47
|
+
restoreState = true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
icon = {
|
|
51
|
+
Icon(destination.icon, contentDescription = destination.label)
|
|
52
|
+
},
|
|
53
|
+
label = { Text(destination.label) },
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
) { innerPadding ->
|
|
59
|
+
NavHost(
|
|
60
|
+
navController = navController,
|
|
61
|
+
startDestination = HomeRoute,
|
|
62
|
+
modifier = Modifier.padding(innerPadding),
|
|
63
|
+
) {
|
|
64
|
+
composable<HomeRoute> { HomeScreen() }
|
|
65
|
+
composable<SearchRoute> { SearchScreen() }
|
|
66
|
+
composable<ProfileRoute> { ProfileScreen() }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
@@ -151,46 +151,11 @@ Batch requests run directly against SQLite. They skip the context, which makes
|
|
|
151
151
|
them fast, but they post no context notifications, so **merge their results
|
|
152
152
|
yourself**.
|
|
153
153
|
|
|
154
|
-
```swift
|
|
155
|
-
@Sendable func publish(_ result: Any?, as key: String) {
|
|
156
|
-
let ids = result as? [NSManagedObjectID] ?? []
|
|
157
|
-
let ui = LibraryStore.shared.container.viewContext
|
|
158
|
-
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: [key: ids], into: [ui])
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
let worker = LibraryStore.shared.makeWorker()
|
|
162
|
-
|
|
163
|
-
// Insert (iOS 13+)
|
|
164
|
-
try await worker.perform {
|
|
165
|
-
let insert = NSBatchInsertRequest(entity: Book.entity(), objects: rows.map { ["isbn": $0.isbn, "title": $0.title] })
|
|
166
|
-
insert.resultType = .objectIDs
|
|
167
|
-
let inserted = try worker.execute(insert) as? NSBatchInsertResult
|
|
168
|
-
publish(inserted?.result, as: NSInsertedObjectsKey)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Delete (iOS 9+)
|
|
172
|
-
try await worker.perform {
|
|
173
|
-
let stale: NSFetchRequest<NSFetchRequestResult> = Loan.fetchRequest()
|
|
174
|
-
stale.predicate = NSPredicate(format: "returnedAt < %@", cutoff as NSDate)
|
|
175
|
-
let delete = NSBatchDeleteRequest(fetchRequest: stale)
|
|
176
|
-
delete.resultType = .resultTypeObjectIDs
|
|
177
|
-
let removed = try worker.execute(delete) as? NSBatchDeleteResult
|
|
178
|
-
publish(removed?.result, as: NSDeletedObjectsKey)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Update (iOS 8+)
|
|
182
|
-
try await worker.perform {
|
|
183
|
-
let update = NSBatchUpdateRequest(entity: Book.entity())
|
|
184
|
-
update.propertiesToUpdate = ["isArchived": true]
|
|
185
|
-
update.resultType = .updatedObjectIDsResultType
|
|
186
|
-
let changed = try worker.execute(update) as? NSBatchUpdateResult
|
|
187
|
-
publish(changed?.result, as: NSUpdatedObjectsKey)
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
154
|
A batch delete does not honor the **Deny** delete rule and skips validation
|
|
192
155
|
beyond the basic delete rules. Check those constraints yourself first.
|
|
193
156
|
|
|
157
|
+
Code for this section: [Core Data code patterns](references/core-data-code.md).
|
|
158
|
+
|
|
194
159
|
## Persistent history
|
|
195
160
|
|
|
196
161
|
History tracking records store-level changes so the app, its extensions and
|
|
@@ -214,58 +179,6 @@ container.persistentStoreDescriptions = [description]
|
|
|
214
179
|
3. Merge each transaction into `viewContext` and move the token forward.
|
|
215
180
|
4. Purge history older than the token.
|
|
216
181
|
|
|
217
|
-
```swift
|
|
218
|
-
final class HistoryReader: NSObject, @unchecked Sendable {
|
|
219
|
-
private let container: NSPersistentContainer
|
|
220
|
-
private let worker: NSManagedObjectContext
|
|
221
|
-
private let tokenKey: String // one per target: "history.app", "history.widget"
|
|
222
|
-
private var lastToken: NSPersistentHistoryToken?
|
|
223
|
-
|
|
224
|
-
init(container: NSPersistentContainer, target: String) {
|
|
225
|
-
self.container = container
|
|
226
|
-
self.worker = container.newBackgroundContext()
|
|
227
|
-
self.tokenKey = "history.\(target)"
|
|
228
|
-
super.init()
|
|
229
|
-
lastToken = Self.loadToken(forKey: tokenKey)
|
|
230
|
-
let center = NotificationCenter.default
|
|
231
|
-
center.addObserver(self, selector: #selector(storeChanged),
|
|
232
|
-
name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
@objc private func storeChanged(_ note: Notification) {
|
|
236
|
-
worker.perform { [self] in
|
|
237
|
-
let fetch = NSPersistentHistoryChangeRequest.fetchHistory(after: lastToken)
|
|
238
|
-
let history = try? worker.execute(fetch) as? NSPersistentHistoryResult
|
|
239
|
-
guard let transactions = history?.result as? [NSPersistentHistoryTransaction] else { return }
|
|
240
|
-
let ui = container.viewContext
|
|
241
|
-
for transaction in transactions {
|
|
242
|
-
let changes = transaction.objectIDNotification().userInfo ?? [:]
|
|
243
|
-
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [ui])
|
|
244
|
-
lastToken = transaction.token
|
|
245
|
-
}
|
|
246
|
-
Self.saveToken(lastToken, forKey: tokenKey)
|
|
247
|
-
if let lastToken {
|
|
248
|
-
_ = try? worker.execute(NSPersistentHistoryChangeRequest.deleteHistory(before: lastToken))
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
private static var defaults: UserDefaults { UserDefaults(suiteName: "group.app.library") ?? .standard }
|
|
254
|
-
|
|
255
|
-
private static func loadToken(forKey key: String) -> NSPersistentHistoryToken? {
|
|
256
|
-
defaults.data(forKey: key).flatMap {
|
|
257
|
-
try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: $0)
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
private static func saveToken(_ token: NSPersistentHistoryToken?, forKey key: String) {
|
|
262
|
-
guard let token else { return }
|
|
263
|
-
let encoded = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true)
|
|
264
|
-
defaults.set(encoded, forKey: key)
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
```
|
|
268
|
-
|
|
269
182
|
`@unchecked Sendable` rests on one guarantee: after `init` (which sets
|
|
270
183
|
`lastToken` before the observer is registered), `lastToken` is read and written
|
|
271
184
|
only inside `worker.perform`, and one context runs its `perform` blocks one at a
|
|
@@ -281,6 +194,8 @@ Keep one token per target (for example in the shared `UserDefaults` suite) so
|
|
|
281
194
|
each process resumes where it stopped after relaunch. Purge only up to the
|
|
282
195
|
point every reader has consumed.
|
|
283
196
|
|
|
197
|
+
Code for this section: [Core Data code patterns](references/core-data-code.md).
|
|
198
|
+
|
|
284
199
|
## Staged migration (iOS 17+)
|
|
285
200
|
|
|
286
201
|
`NSStagedMigrationManager` applies an ordered list of stages, each either
|
|
@@ -292,40 +207,6 @@ lightweight or custom.
|
|
|
292
207
|
- `NSCustomMigrationStage(migratingFrom:to:)` takes a source and a destination
|
|
293
208
|
reference and offers `label` and `willMigrateHandler` (called with the manager and stage).
|
|
294
209
|
|
|
295
|
-
```swift
|
|
296
|
-
let v1 = NSManagedObjectModelReference(name: "Library_v1", in: .main, versionChecksum: "k3Qp...v1")
|
|
297
|
-
let v2 = NSManagedObjectModelReference(name: "Library_v2", in: .main, versionChecksum: "R8bz...v2")
|
|
298
|
-
let v3Checksum = "Yt5m...v3"
|
|
299
|
-
|
|
300
|
-
let splitAuthor = NSCustomMigrationStage(migratingFrom: v1, to: v2)
|
|
301
|
-
splitAuthor.label = "Split author into first and last name"
|
|
302
|
-
splitAuthor.willMigrateHandler = { manager, _ in
|
|
303
|
-
guard let store = manager.container else { return }
|
|
304
|
-
let context = store.newBackgroundContext()
|
|
305
|
-
try context.performAndWait {
|
|
306
|
-
let request = NSFetchRequest<NSManagedObject>(entityName: "Book")
|
|
307
|
-
for book in try context.fetch(request) {
|
|
308
|
-
let full = book.value(forKey: "author") as? String ?? ""
|
|
309
|
-
let parts = full.split(separator: " ", maxSplits: 1).map(String.init)
|
|
310
|
-
book.setValue(parts.first, forKey: "authorFirst")
|
|
311
|
-
book.setValue(parts.count > 1 ? parts[1] : nil, forKey: "authorLast")
|
|
312
|
-
}
|
|
313
|
-
try context.save()
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
let addIndexes = NSLightweightMigrationStage([v3Checksum])
|
|
318
|
-
addIndexes.label = "v2 to v3: add indexes"
|
|
319
|
-
|
|
320
|
-
let manager = NSStagedMigrationManager([splitAuthor, addIndexes])
|
|
321
|
-
let description = container.persistentStoreDescriptions.first ?? NSPersistentStoreDescription()
|
|
322
|
-
description.setOption(manager, forKey: NSPersistentStoreStagedMigrationManagerOptionKey)
|
|
323
|
-
container.persistentStoreDescriptions = [description]
|
|
324
|
-
container.loadPersistentStores { _, error in
|
|
325
|
-
if let error { MigrationLog.failed(error) }
|
|
326
|
-
}
|
|
327
|
-
```
|
|
328
|
-
|
|
329
210
|
The option must be on the store description **before** `loadPersistentStores`.
|
|
330
211
|
Read the checksums from the compiled `.momd` (for example
|
|
331
212
|
`NSManagedObjectModel.versionChecksum`), never type a model name in their place.
|
|
@@ -333,6 +214,8 @@ Read the checksums from the compiled `.momd` (for example
|
|
|
333
214
|
Older deployment targets fall back to inferred mapping
|
|
334
215
|
(`NSInferMappingModelAutomaticallyOption`) or hand-written mapping models.
|
|
335
216
|
|
|
217
|
+
Code for this section: [Core Data code patterns](references/core-data-code.md).
|
|
218
|
+
|
|
336
219
|
## Composite attributes (iOS 17+)
|
|
337
220
|
|
|
338
221
|
`NSCompositeAttributeDescription` groups several sub-attributes into one
|
|
@@ -353,44 +236,12 @@ from this side:
|
|
|
353
236
|
|
|
354
237
|
## Testing with an in-memory store
|
|
355
238
|
|
|
356
|
-
```swift
|
|
357
|
-
import Testing
|
|
358
|
-
import CoreData
|
|
359
|
-
|
|
360
|
-
enum TestModel {
|
|
361
|
-
nonisolated(unsafe) static let shared: NSManagedObjectModel = { // loaded once, never mutated
|
|
362
|
-
let compiled = Bundle(for: LibraryStore.self).url(forResource: "Library", withExtension: "momd")
|
|
363
|
-
return compiled.flatMap(NSManagedObjectModel.init(contentsOf:)) ?? NSManagedObjectModel()
|
|
364
|
-
}()
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
func makeMemoryContainer() throws -> NSPersistentContainer {
|
|
368
|
-
let container = NSPersistentContainer(name: "Library", managedObjectModel: TestModel.shared)
|
|
369
|
-
let memory = NSPersistentStoreDescription()
|
|
370
|
-
memory.type = NSInMemoryStoreType
|
|
371
|
-
container.persistentStoreDescriptions = [memory]
|
|
372
|
-
var failure: Error?
|
|
373
|
-
container.loadPersistentStores { _, problem in failure = problem }
|
|
374
|
-
if let failure { throw failure }
|
|
375
|
-
return container
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
@Test func savingABookPersistsIt() throws {
|
|
379
|
-
let context = try makeMemoryContainer().viewContext
|
|
380
|
-
let book = Book(context: context)
|
|
381
|
-
book.title = "Dune"
|
|
382
|
-
try context.save()
|
|
383
|
-
|
|
384
|
-
let fetched = try context.fetch(Book.fetchRequest())
|
|
385
|
-
#expect(fetched.count == 1)
|
|
386
|
-
#expect(fetched.first?.title == "Dune")
|
|
387
|
-
}
|
|
388
|
-
```
|
|
389
|
-
|
|
390
239
|
Load the model once and pass the same `NSManagedObjectModel` to every test
|
|
391
240
|
container; loading it per test produces "multiple entity descriptions claim"
|
|
392
241
|
duplicate-entity warnings.
|
|
393
242
|
|
|
243
|
+
Code for this section: [Core Data code patterns](references/core-data-code.md).
|
|
244
|
+
|
|
394
245
|
## Common mistakes
|
|
395
246
|
|
|
396
247
|
| Mistake | Fix |
|
|
@@ -421,6 +272,7 @@ duplicate-entity warnings.
|
|
|
421
272
|
|
|
422
273
|
## References
|
|
423
274
|
|
|
275
|
+
- [Core Data code patterns](references/core-data-code.md): open when writing batch requests, consuming persistent history, setting up staged migration or building an in-memory test container.
|
|
424
276
|
- [Core Data](https://developer.apple.com/documentation/coredata)
|
|
425
277
|
- [NSPersistentContainer](https://developer.apple.com/documentation/coredata/nspersistentcontainer)
|
|
426
278
|
- [NSManagedObjectContext](https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Core Data code patterns
|
|
2
|
+
|
|
3
|
+
<!-- toc -->
|
|
4
|
+
- [Batch requests](#batch-requests)
|
|
5
|
+
- [Staged migration (iOS 17+)](#staged-migration-ios-17)
|
|
6
|
+
- [Testing with an in-memory store](#testing-with-an-in-memory-store)
|
|
7
|
+
<!-- /toc -->
|
|
8
|
+
|
|
9
|
+
Full code for batch requests, persistent history consumption, staged migration and in-memory test containers. SKILL.md keeps the rules; this file holds the worked code.
|
|
10
|
+
|
|
11
|
+
## Batch requests
|
|
12
|
+
|
|
13
|
+
Batch requests run directly against SQLite. They skip the context, which makes
|
|
14
|
+
them fast, but they post no context notifications, so **merge their results
|
|
15
|
+
yourself**.
|
|
16
|
+
|
|
17
|
+
```swift
|
|
18
|
+
@Sendable func publish(_ result: Any?, as key: String) {
|
|
19
|
+
let ids = result as? [NSManagedObjectID] ?? []
|
|
20
|
+
let ui = LibraryStore.shared.container.viewContext
|
|
21
|
+
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: [key: ids], into: [ui])
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let worker = LibraryStore.shared.makeWorker()
|
|
25
|
+
|
|
26
|
+
// Insert (iOS 13+)
|
|
27
|
+
try await worker.perform {
|
|
28
|
+
let insert = NSBatchInsertRequest(entity: Book.entity(), objects: rows.map { ["isbn": $0.isbn, "title": $0.title] })
|
|
29
|
+
insert.resultType = .objectIDs
|
|
30
|
+
let inserted = try worker.execute(insert) as? NSBatchInsertResult
|
|
31
|
+
publish(inserted?.result, as: NSInsertedObjectsKey)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Delete (iOS 9+)
|
|
35
|
+
try await worker.perform {
|
|
36
|
+
let stale: NSFetchRequest<NSFetchRequestResult> = Loan.fetchRequest()
|
|
37
|
+
stale.predicate = NSPredicate(format: "returnedAt < %@", cutoff as NSDate)
|
|
38
|
+
let delete = NSBatchDeleteRequest(fetchRequest: stale)
|
|
39
|
+
delete.resultType = .resultTypeObjectIDs
|
|
40
|
+
let removed = try worker.execute(delete) as? NSBatchDeleteResult
|
|
41
|
+
publish(removed?.result, as: NSDeletedObjectsKey)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Update (iOS 8+)
|
|
45
|
+
try await worker.perform {
|
|
46
|
+
let update = NSBatchUpdateRequest(entity: Book.entity())
|
|
47
|
+
update.propertiesToUpdate = ["isArchived": true]
|
|
48
|
+
update.resultType = .updatedObjectIDsResultType
|
|
49
|
+
let changed = try worker.execute(update) as? NSBatchUpdateResult
|
|
50
|
+
publish(changed?.result, as: NSUpdatedObjectsKey)
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A batch delete does not honor the **Deny** delete rule and skips validation
|
|
55
|
+
beyond the basic delete rules. Check those constraints yourself first.
|
|
56
|
+
|
|
57
|
+
### Consume it
|
|
58
|
+
|
|
59
|
+
1. Observe `.NSPersistentStoreRemoteChange`, with the container's persistent store coordinator as the object.
|
|
60
|
+
2. On a background context, fetch transactions after your last token.
|
|
61
|
+
3. Merge each transaction into `viewContext` and move the token forward.
|
|
62
|
+
4. Purge history older than the token.
|
|
63
|
+
|
|
64
|
+
```swift
|
|
65
|
+
final class HistoryReader: NSObject, @unchecked Sendable {
|
|
66
|
+
private let container: NSPersistentContainer
|
|
67
|
+
private let worker: NSManagedObjectContext
|
|
68
|
+
private let tokenKey: String // one per target: "history.app", "history.widget"
|
|
69
|
+
private var lastToken: NSPersistentHistoryToken?
|
|
70
|
+
|
|
71
|
+
init(container: NSPersistentContainer, target: String) {
|
|
72
|
+
self.container = container
|
|
73
|
+
self.worker = container.newBackgroundContext()
|
|
74
|
+
self.tokenKey = "history.\(target)"
|
|
75
|
+
super.init()
|
|
76
|
+
lastToken = Self.loadToken(forKey: tokenKey)
|
|
77
|
+
let center = NotificationCenter.default
|
|
78
|
+
center.addObserver(self, selector: #selector(storeChanged),
|
|
79
|
+
name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@objc private func storeChanged(_ note: Notification) {
|
|
83
|
+
worker.perform { [self] in
|
|
84
|
+
let fetch = NSPersistentHistoryChangeRequest.fetchHistory(after: lastToken)
|
|
85
|
+
let history = try? worker.execute(fetch) as? NSPersistentHistoryResult
|
|
86
|
+
guard let transactions = history?.result as? [NSPersistentHistoryTransaction] else { return }
|
|
87
|
+
let ui = container.viewContext
|
|
88
|
+
for transaction in transactions {
|
|
89
|
+
let changes = transaction.objectIDNotification().userInfo ?? [:]
|
|
90
|
+
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [ui])
|
|
91
|
+
lastToken = transaction.token
|
|
92
|
+
}
|
|
93
|
+
Self.saveToken(lastToken, forKey: tokenKey)
|
|
94
|
+
if let lastToken {
|
|
95
|
+
_ = try? worker.execute(NSPersistentHistoryChangeRequest.deleteHistory(before: lastToken))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private static var defaults: UserDefaults { UserDefaults(suiteName: "group.app.library") ?? .standard }
|
|
101
|
+
|
|
102
|
+
private static func loadToken(forKey key: String) -> NSPersistentHistoryToken? {
|
|
103
|
+
defaults.data(forKey: key).flatMap {
|
|
104
|
+
try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: $0)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private static func saveToken(_ token: NSPersistentHistoryToken?, forKey key: String) {
|
|
109
|
+
guard let token else { return }
|
|
110
|
+
let encoded = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true)
|
|
111
|
+
defaults.set(encoded, forKey: key)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`@unchecked Sendable` rests on one guarantee: after `init` (which sets
|
|
117
|
+
`lastToken` before the observer is registered), `lastToken` is read and written
|
|
118
|
+
only inside `worker.perform`, and one context runs its `perform` blocks one at a
|
|
119
|
+
time on its private queue. Creating a new background context per notification
|
|
120
|
+
would break that, because two notifications could then run on two queues at
|
|
121
|
+
once.
|
|
122
|
+
|
|
123
|
+
The static merge call is safe from the worker queue; calling
|
|
124
|
+
`viewContext.mergeChanges(fromContextDidSave: transaction.objectIDNotification())`
|
|
125
|
+
inside `viewContext.perform` is the equivalent instance form.
|
|
126
|
+
|
|
127
|
+
Keep one token per target (for example in the shared `UserDefaults` suite) so
|
|
128
|
+
each process resumes where it stopped after relaunch. Purge only up to the
|
|
129
|
+
point every reader has consumed.
|
|
130
|
+
|
|
131
|
+
## Staged migration (iOS 17+)
|
|
132
|
+
|
|
133
|
+
`NSStagedMigrationManager` applies an ordered list of stages, each either
|
|
134
|
+
lightweight or custom.
|
|
135
|
+
|
|
136
|
+
- `NSLightweightMigrationStage` takes the **version checksums** (`[String]`) of
|
|
137
|
+
compiled model versions, not their names, and has a `label`.
|
|
138
|
+
- `NSManagedObjectModelReference(name:in:versionChecksum:)` points to one model version.
|
|
139
|
+
- `NSCustomMigrationStage(migratingFrom:to:)` takes a source and a destination
|
|
140
|
+
reference and offers `label` and `willMigrateHandler` (called with the manager and stage).
|
|
141
|
+
|
|
142
|
+
```swift
|
|
143
|
+
let v1 = NSManagedObjectModelReference(name: "Library_v1", in: .main, versionChecksum: "k3Qp...v1")
|
|
144
|
+
let v2 = NSManagedObjectModelReference(name: "Library_v2", in: .main, versionChecksum: "R8bz...v2")
|
|
145
|
+
let v3Checksum = "Yt5m...v3"
|
|
146
|
+
|
|
147
|
+
let splitAuthor = NSCustomMigrationStage(migratingFrom: v1, to: v2)
|
|
148
|
+
splitAuthor.label = "Split author into first and last name"
|
|
149
|
+
splitAuthor.willMigrateHandler = { manager, _ in
|
|
150
|
+
guard let store = manager.container else { return }
|
|
151
|
+
let context = store.newBackgroundContext()
|
|
152
|
+
try context.performAndWait {
|
|
153
|
+
let request = NSFetchRequest<NSManagedObject>(entityName: "Book")
|
|
154
|
+
for book in try context.fetch(request) {
|
|
155
|
+
let full = book.value(forKey: "author") as? String ?? ""
|
|
156
|
+
let parts = full.split(separator: " ", maxSplits: 1).map(String.init)
|
|
157
|
+
book.setValue(parts.first, forKey: "authorFirst")
|
|
158
|
+
book.setValue(parts.count > 1 ? parts[1] : nil, forKey: "authorLast")
|
|
159
|
+
}
|
|
160
|
+
try context.save()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let addIndexes = NSLightweightMigrationStage([v3Checksum])
|
|
165
|
+
addIndexes.label = "v2 to v3: add indexes"
|
|
166
|
+
|
|
167
|
+
let manager = NSStagedMigrationManager([splitAuthor, addIndexes])
|
|
168
|
+
let description = container.persistentStoreDescriptions.first ?? NSPersistentStoreDescription()
|
|
169
|
+
description.setOption(manager, forKey: NSPersistentStoreStagedMigrationManagerOptionKey)
|
|
170
|
+
container.persistentStoreDescriptions = [description]
|
|
171
|
+
container.loadPersistentStores { _, error in
|
|
172
|
+
if let error { MigrationLog.failed(error) }
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The option must be on the store description **before** `loadPersistentStores`.
|
|
177
|
+
Read the checksums from the compiled `.momd` (for example
|
|
178
|
+
`NSManagedObjectModel.versionChecksum`), never type a model name in their place.
|
|
179
|
+
|
|
180
|
+
Older deployment targets fall back to inferred mapping
|
|
181
|
+
(`NSInferMappingModelAutomaticallyOption`) or hand-written mapping models.
|
|
182
|
+
|
|
183
|
+
## Testing with an in-memory store
|
|
184
|
+
|
|
185
|
+
```swift
|
|
186
|
+
import Testing
|
|
187
|
+
import CoreData
|
|
188
|
+
|
|
189
|
+
enum TestModel {
|
|
190
|
+
nonisolated(unsafe) static let shared: NSManagedObjectModel = { // loaded once, never mutated
|
|
191
|
+
let compiled = Bundle(for: LibraryStore.self).url(forResource: "Library", withExtension: "momd")
|
|
192
|
+
return compiled.flatMap(NSManagedObjectModel.init(contentsOf:)) ?? NSManagedObjectModel()
|
|
193
|
+
}()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
func makeMemoryContainer() throws -> NSPersistentContainer {
|
|
197
|
+
let container = NSPersistentContainer(name: "Library", managedObjectModel: TestModel.shared)
|
|
198
|
+
let memory = NSPersistentStoreDescription()
|
|
199
|
+
memory.type = NSInMemoryStoreType
|
|
200
|
+
container.persistentStoreDescriptions = [memory]
|
|
201
|
+
var failure: Error?
|
|
202
|
+
container.loadPersistentStores { _, problem in failure = problem }
|
|
203
|
+
if let failure { throw failure }
|
|
204
|
+
return container
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@Test func savingABookPersistsIt() throws {
|
|
208
|
+
let context = try makeMemoryContainer().viewContext
|
|
209
|
+
let book = Book(context: context)
|
|
210
|
+
book.title = "Dune"
|
|
211
|
+
try context.save()
|
|
212
|
+
|
|
213
|
+
let fetched = try context.fetch(Book.fetchRequest())
|
|
214
|
+
#expect(fetched.count == 1)
|
|
215
|
+
#expect(fetched.first?.title == "Dune")
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Load the model once and pass the same `NSManagedObjectModel` to every test
|
|
220
|
+
container; loading it per test produces "multiple entity descriptions claim"
|
|
221
|
+
duplicate-entity warnings.
|
|
@@ -61,48 +61,7 @@ disable the feature instead of starting a session that will fail.
|
|
|
61
61
|
The NDEF session is the shortest path when all you need is a URL, some text or
|
|
62
62
|
a MIME blob.
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
import CoreNFC
|
|
66
|
-
|
|
67
|
-
final class BadgeReader: NSObject, NFCNDEFReaderSessionDelegate {
|
|
68
|
-
private var session: NFCNDEFReaderSession?
|
|
69
|
-
|
|
70
|
-
func scan() {
|
|
71
|
-
guard NFCNDEFReaderSession.readingAvailable else { return }
|
|
72
|
-
session = NFCNDEFReaderSession(
|
|
73
|
-
delegate: self,
|
|
74
|
-
queue: nil,
|
|
75
|
-
invalidateAfterFirstRead: false
|
|
76
|
-
)
|
|
77
|
-
session?.alertMessage = "Hold the top of your iPhone near the badge."
|
|
78
|
-
session?.begin()
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {}
|
|
82
|
-
|
|
83
|
-
func readerSession(_ reader: NFCNDEFReaderSession, didDetectNDEFs found: [NFCNDEFMessage]) {
|
|
84
|
-
for message in found {
|
|
85
|
-
for record in message.records {
|
|
86
|
-
handle(record)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
func readerSession(_ reader: NFCNDEFReaderSession, didInvalidateWithError error: any Error) {
|
|
92
|
-
if let nfcError = error as? NFCReaderError,
|
|
93
|
-
nfcError.code == .readerSessionInvalidationErrorFirstNDEFTagRead
|
|
94
|
-
|| nfcError.code == .readerSessionInvalidationErrorUserCanceled {
|
|
95
|
-
// Normal end of a scan, not a failure.
|
|
96
|
-
} else {
|
|
97
|
-
report(error)
|
|
98
|
-
}
|
|
99
|
-
self.session = nil
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
private func handle(_ record: NFCNDEFPayload) {}
|
|
103
|
-
private func report(_ error: Error) {}
|
|
104
|
-
}
|
|
105
|
-
```
|
|
64
|
+
Code: [Reading NDEF data](references/code-examples.md#reading-ndef-data).
|
|
106
65
|
|
|
107
66
|
The first-read and user-cancel codes are ordinary endings. Only other codes
|
|
108
67
|
deserve an alert, and the session reference is cleared in every case.
|
|
@@ -204,53 +163,7 @@ Payment application identifiers are off limits for this session. For eligible
|
|
|
204
163
|
payment use cases in the EU, Apple documents a separate
|
|
205
164
|
`NFCPaymentTagReaderSession`.
|
|
206
165
|
|
|
207
|
-
|
|
208
|
-
final class CardReader: NSObject, NFCTagReaderSessionDelegate {
|
|
209
|
-
private var session: NFCTagReaderSession?
|
|
210
|
-
|
|
211
|
-
func scan() {
|
|
212
|
-
guard NFCTagReaderSession.readingAvailable else { return }
|
|
213
|
-
session = NFCTagReaderSession(pollingOption: [.iso14443, .iso15693], delegate: self, queue: nil)
|
|
214
|
-
session?.alertMessage = "Hold your transit card near the phone."
|
|
215
|
-
session?.begin()
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {}
|
|
219
|
-
|
|
220
|
-
func tagReaderSession(_ reader: NFCTagReaderSession, didDetect found: [NFCTag]) {
|
|
221
|
-
if found.count > 1 {
|
|
222
|
-
retryAfterMultipleTags(reader)
|
|
223
|
-
return
|
|
224
|
-
}
|
|
225
|
-
guard !found.isEmpty else {
|
|
226
|
-
reader.restartPolling()
|
|
227
|
-
return
|
|
228
|
-
}
|
|
229
|
-
nonisolated(unsafe) let reader = reader
|
|
230
|
-
nonisolated(unsafe) let tags = found
|
|
231
|
-
Task {
|
|
232
|
-
let tag = tags[0]
|
|
233
|
-
do {
|
|
234
|
-
try await reader.connect(to: tag)
|
|
235
|
-
} catch {
|
|
236
|
-
reader.invalidate(errorMessage: error.localizedDescription)
|
|
237
|
-
return
|
|
238
|
-
}
|
|
239
|
-
switch tag {
|
|
240
|
-
case .iso7816(let card): await select(card, in: reader)
|
|
241
|
-
case .miFare(let chip): await inspect(chip, in: reader)
|
|
242
|
-
case .iso15693(let label): await readBlocks(label, in: reader)
|
|
243
|
-
case .feliCa(let card): await readSystemCodes(card, in: reader)
|
|
244
|
-
@unknown default: reader.invalidate(errorMessage: "This tag type is not handled.")
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
func tagReaderSession(_ reader: NFCTagReaderSession, didInvalidateWithError error: any Error) {
|
|
250
|
-
session = nil
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
```
|
|
166
|
+
Code: [Native tag access](references/code-examples.md#native-tag-access).
|
|
254
167
|
|
|
255
168
|
The task receives the whole `tags` array rather than a single `NFCTag`: with
|
|
256
169
|
the enum value itself as the `nonisolated(unsafe)` copy, Swift 6.3 still
|
|
@@ -441,3 +354,4 @@ func scene(_ scene: UIScene, continue activity: NSUserActivity) {
|
|
|
441
354
|
- keys: [formats entitlement](https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.nfc.readersession.formats),
|
|
442
355
|
[ISO 7816 AIDs](https://developer.apple.com/documentation/bundleresources/information-property-list/com.apple.developer.nfc.readersession.iso7816.select-identifiers),
|
|
443
356
|
[FeliCa system codes](https://developer.apple.com/documentation/bundleresources/information-property-list/com.apple.developer.nfc.readersession.felica.systemcodes)
|
|
357
|
+
- [Code examples](references/code-examples.md): open for the full code behind Reading NDEF data, Native tag access.
|