@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.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 +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -3,38 +3,36 @@
|
|
|
3
3
|
"evals": [
|
|
4
4
|
{
|
|
5
5
|
"id": 1,
|
|
6
|
-
"prompt": "
|
|
7
|
-
"expected_output": "
|
|
8
|
-
"files": [],
|
|
6
|
+
"prompt": "Our iOS 26 carpool app shows the user's contacts so they can invite riders. Some testers chose to share only a handful of contacts and now say the list is nearly empty and there is no way to add more. How should the app treat this permission state and let them share more?",
|
|
7
|
+
"expected_output": "Identifies the .limited authorization state, treats it like .authorized for proceeding, explains that the app sees and edits just the shared contacts plus any it created itself, and offers ContactAccessButton or the contactAccessPicker modifier so people can share more. Distinguishes managing limited access from picking a contact with CNContactPickerViewController.",
|
|
9
8
|
"assertions": [
|
|
10
|
-
"
|
|
11
|
-
"Explains that .limited
|
|
12
|
-
"
|
|
13
|
-
"
|
|
9
|
+
"Lets the app continue in the .limited state just as in .authorized",
|
|
10
|
+
"Explains that .limited reaches shared contacts and contacts the app itself created, nothing else",
|
|
11
|
+
"Offers ContactAccessButton or contactAccessPicker to expand access",
|
|
12
|
+
"Separates limited-access management from one-off selection with the contact picker"
|
|
14
13
|
]
|
|
15
14
|
},
|
|
16
15
|
{
|
|
17
16
|
"id": 2,
|
|
18
|
-
"prompt": "
|
|
19
|
-
"expected_output": "
|
|
20
|
-
"files": [],
|
|
17
|
+
"prompt": "Here is our plan for a CRM contact list: fetch CNContactCompleteNameKey for names, CNContactImageDataKey for the round avatars, and CNContactNoteKey to show a note preview in each row. Anything wrong?",
|
|
18
|
+
"expected_output": "Rejects CNContactCompleteNameKey in favour of CNContactFormatter.descriptorForRequiredKeys(for: .fullName) or individual name keys, swaps the full image for CNContactThumbnailImageDataKey in list avatars, and points out that reading notes (CNContactNoteKey) is gated by an Apple-approved entitlement, com.apple.developer.contacts.notes, granted before App Store distribution.",
|
|
21
19
|
"assertions": [
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
20
|
+
"Replaces CNContactCompleteNameKey with the formatter descriptor or individual name keys",
|
|
21
|
+
"Uses thumbnail image data rather than full-resolution image data for list avatars",
|
|
22
|
+
"Ties CNContactNoteKey to the com.apple.developer.contacts.notes entitlement",
|
|
23
|
+
"Notes that the notes entitlement needs Apple approval before public distribution"
|
|
26
24
|
]
|
|
27
25
|
},
|
|
28
26
|
{
|
|
29
27
|
"id": 3,
|
|
30
|
-
"prompt": "
|
|
31
|
-
"expected_output": "
|
|
32
|
-
"files": [],
|
|
28
|
+
"prompt": "We want users to pick one email address from their contacts without granting us Contacts access, and we also want to sync incrementally by calling store.enumerateChanges(matching:) when the address book changes. Sketch both parts.",
|
|
29
|
+
"expected_output": "Uses CNContactPickerViewController, which works without any Contacts permission and hands back just the chosen item, configuring predicateForEnablingContact and predicateForSelectionOfProperty (and displayedPropertyKeys) before presenting it. Rejects enumerateChanges(matching:) as nonexistent, explains the change-history enumerator is an Objective-C selector unavailable in Swift, and recommends refetching on CNContactStoreDidChange or wrapping an Objective-C bridge.",
|
|
33
30
|
"assertions": [
|
|
34
|
-
"States that
|
|
35
|
-
"
|
|
36
|
-
"Rejects
|
|
37
|
-
"
|
|
31
|
+
"States that the picker needs no Contacts authorization and returns only what was selected",
|
|
32
|
+
"Sets predicateForEnablingContact and predicateForSelectionOfProperty before presentation",
|
|
33
|
+
"Rejects enumerateChanges(matching:) as an API that does not exist",
|
|
34
|
+
"Says change history comes from an Objective-C selector that Swift cannot call",
|
|
35
|
+
"Recommends refetching on CNContactStoreDidChange or an isolated Objective-C bridge"
|
|
38
36
|
]
|
|
39
37
|
}
|
|
40
38
|
]
|
|
@@ -1,409 +1,270 @@
|
|
|
1
|
-
# Contacts
|
|
1
|
+
# Contacts patterns
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Longer examples for the `contacts-framework` skill ([../SKILL.md](../SKILL.md)).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## SwiftUI integration
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- [Multi-Select Contact Picker](#multi-select-contact-picker)
|
|
9
|
-
- [Search and Filtering](#search-and-filtering)
|
|
10
|
-
- [vCard Import and Export](#vcard-import-and-export)
|
|
11
|
-
- [Contact Groups](#contact-groups)
|
|
12
|
-
- [Change Notifications and Swift Boundaries](#change-notifications-and-swift-boundaries)
|
|
13
|
-
|
|
14
|
-
## Contacts SwiftUI Integration
|
|
15
|
-
|
|
16
|
-
### Contact Manager with `@Observable`
|
|
7
|
+
### An observable address-book model
|
|
17
8
|
|
|
18
9
|
```swift
|
|
19
10
|
@preconcurrency import Contacts
|
|
20
|
-
import
|
|
21
|
-
import SwiftUI
|
|
22
|
-
import UIKit
|
|
11
|
+
import Observation
|
|
23
12
|
|
|
24
13
|
@Observable
|
|
25
14
|
@MainActor
|
|
26
|
-
final class
|
|
27
|
-
let
|
|
28
|
-
|
|
29
|
-
var
|
|
30
|
-
var
|
|
31
|
-
var
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
15
|
+
final class AddressBookModel {
|
|
16
|
+
@ObservationIgnored let book = CNContactStore()
|
|
17
|
+
private(set) var people: [CNContact] = []
|
|
18
|
+
private(set) var canRead = false
|
|
19
|
+
private(set) var isLimited = false
|
|
20
|
+
private(set) var status = CNContactStore.authorizationStatus(for: .contacts)
|
|
21
|
+
|
|
22
|
+
func refreshStatus() {
|
|
23
|
+
status = CNContactStore.authorizationStatus(for: .contacts)
|
|
24
|
+
canRead = status == .authorized || status == .limited
|
|
25
|
+
isLimited = status == .limited
|
|
36
26
|
}
|
|
37
27
|
|
|
38
|
-
func
|
|
39
|
-
_ = try await
|
|
40
|
-
|
|
28
|
+
func askForAccess() async {
|
|
29
|
+
_ = try? await book.requestAccess(for: .contacts)
|
|
30
|
+
refreshStatus() // the status is the source of truth, not the Bool
|
|
41
31
|
}
|
|
42
32
|
|
|
43
|
-
func
|
|
44
|
-
|
|
45
|
-
canAccessContacts = status == .authorized || status == .limited
|
|
46
|
-
hasLimitedAccess = status == .limited
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
func loadContacts() async throws {
|
|
50
|
-
guard canAccessContacts else { return }
|
|
51
|
-
|
|
33
|
+
func reload() async {
|
|
34
|
+
guard canRead else { return }
|
|
52
35
|
let keys: [CNKeyDescriptor] = [
|
|
53
|
-
|
|
54
|
-
CNContactFamilyNameKey as CNKeyDescriptor,
|
|
36
|
+
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
|
|
55
37
|
CNContactPhoneNumbersKey as CNKeyDescriptor,
|
|
56
38
|
CNContactEmailAddressesKey as CNKeyDescriptor,
|
|
57
|
-
CNContactThumbnailImageDataKey as CNKeyDescriptor
|
|
58
|
-
CNContactFormatter.descriptorForRequiredKeys(for: .fullName)
|
|
39
|
+
CNContactThumbnailImageDataKey as CNKeyDescriptor
|
|
59
40
|
]
|
|
60
|
-
|
|
61
|
-
contacts = try await Task.detached { [store] in
|
|
41
|
+
let loaded = try? await Task.detached { [book] in
|
|
62
42
|
let request = CNContactFetchRequest(keysToFetch: keys)
|
|
63
|
-
request.sortOrder = .
|
|
64
|
-
var
|
|
65
|
-
try
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
return results
|
|
43
|
+
request.sortOrder = .userDefault
|
|
44
|
+
var rows: [CNContact] = []
|
|
45
|
+
try book.enumerateContacts(with: request) { person, _ in rows.append(person) }
|
|
46
|
+
return rows
|
|
69
47
|
}.value
|
|
48
|
+
people = loaded ?? []
|
|
70
49
|
}
|
|
71
50
|
|
|
72
|
-
func
|
|
73
|
-
CNContactFormatter.string(from:
|
|
74
|
-
?? "\(
|
|
51
|
+
func label(for person: CNContact) -> String {
|
|
52
|
+
CNContactFormatter.string(from: person, style: .fullName)
|
|
53
|
+
?? "\(person.givenName) \(person.familyName)"
|
|
75
54
|
}
|
|
76
55
|
}
|
|
77
56
|
```
|
|
78
57
|
|
|
79
|
-
###
|
|
58
|
+
### A list that respects limited access
|
|
80
59
|
|
|
81
60
|
```swift
|
|
82
|
-
|
|
83
|
-
|
|
61
|
+
import ContactsUI
|
|
62
|
+
import SwiftUI
|
|
63
|
+
|
|
64
|
+
struct PeopleList: View {
|
|
65
|
+
@Environment(AddressBookModel.self) private var model
|
|
66
|
+
@State private var managingAccess = false
|
|
67
|
+
private let avatarSide: CGFloat = 40
|
|
84
68
|
|
|
85
69
|
var body: some View {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
70
|
+
Group {
|
|
71
|
+
if model.canRead {
|
|
72
|
+
List(model.people, id: \.identifier) { person in
|
|
73
|
+
HStack {
|
|
74
|
+
avatar(for: person)
|
|
75
|
+
VStack(alignment: .leading) {
|
|
76
|
+
Text(model.label(for: person))
|
|
77
|
+
if let phone = person.phoneNumbers.first?.value.stringValue {
|
|
78
|
+
Text(phone)
|
|
79
|
+
.font(.footnote)
|
|
80
|
+
.foregroundStyle(.tertiary)
|
|
81
|
+
}
|
|
96
82
|
}
|
|
97
|
-
.buttonStyle(.borderedProminent)
|
|
98
83
|
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if
|
|
102
|
-
|
|
84
|
+
}
|
|
85
|
+
.toolbar {
|
|
86
|
+
if model.isLimited {
|
|
87
|
+
Button("Share More Contacts") { managingAccess = true }
|
|
103
88
|
}
|
|
104
89
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
.task {
|
|
108
|
-
manager.checkAuthorization()
|
|
109
|
-
if manager.canAccessContacts {
|
|
110
|
-
try? await manager.loadContacts()
|
|
90
|
+
.contactAccessPicker(isPresented: $managingAccess) { newlyShared in
|
|
91
|
+
if !newlyShared.isEmpty { Task { await model.reload() } }
|
|
111
92
|
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
HStack {
|
|
119
|
-
contactAvatar(contact)
|
|
120
|
-
VStack(alignment: .leading) {
|
|
121
|
-
Text(manager.formattedName(for: contact))
|
|
122
|
-
.font(.body)
|
|
123
|
-
if let phone = contact.phoneNumbers.first?.value.stringValue {
|
|
124
|
-
Text(phone)
|
|
125
|
-
.font(.caption)
|
|
126
|
-
.foregroundStyle(.secondary)
|
|
127
|
-
}
|
|
93
|
+
} else {
|
|
94
|
+
ContentUnavailableView {
|
|
95
|
+
Label("Contacts Unavailable", systemImage: "person.crop.circle.badge.xmark")
|
|
96
|
+
} actions: {
|
|
97
|
+
Button("Allow Access") { Task { await model.askForAccess() } }
|
|
98
|
+
.buttonStyle(.borderedProminent)
|
|
128
99
|
}
|
|
129
100
|
}
|
|
130
101
|
}
|
|
102
|
+
.task {
|
|
103
|
+
model.refreshStatus()
|
|
104
|
+
await model.reload()
|
|
105
|
+
}
|
|
131
106
|
}
|
|
132
107
|
|
|
133
108
|
@ViewBuilder
|
|
134
|
-
private func
|
|
135
|
-
if let
|
|
136
|
-
|
|
137
|
-
Image(uiImage: uiImage)
|
|
109
|
+
private func avatar(for person: CNContact) -> some View {
|
|
110
|
+
if let data = person.thumbnailImageData, let image = UIImage(data: data) {
|
|
111
|
+
Image(uiImage: image)
|
|
138
112
|
.resizable()
|
|
139
113
|
.scaledToFill()
|
|
140
|
-
.frame(width:
|
|
141
|
-
.clipShape(
|
|
114
|
+
.frame(width: avatarSide, height: avatarSide)
|
|
115
|
+
.clipShape(.circle)
|
|
142
116
|
} else {
|
|
143
|
-
Image(systemName: "person.circle
|
|
117
|
+
Image(systemName: "person.circle")
|
|
144
118
|
.resizable()
|
|
145
|
-
.frame(width:
|
|
146
|
-
.foregroundStyle(.
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
@State private var isPresentingContactAccessPicker = false
|
|
151
|
-
|
|
152
|
-
private var limitedAccessControls: some View {
|
|
153
|
-
Button {
|
|
154
|
-
isPresentingContactAccessPicker = true
|
|
155
|
-
} label: {
|
|
156
|
-
Label("Add Contacts", systemImage: "person.crop.circle.badge.plus")
|
|
157
|
-
}
|
|
158
|
-
.contactAccessPicker(isPresented: $isPresentingContactAccessPicker) { identifiers in
|
|
159
|
-
guard !identifiers.isEmpty else { return }
|
|
160
|
-
Task { try? await manager.loadContacts() }
|
|
119
|
+
.frame(width: avatarSide, height: avatarSide)
|
|
120
|
+
.foregroundStyle(.gray)
|
|
161
121
|
}
|
|
162
122
|
}
|
|
163
123
|
}
|
|
164
124
|
```
|
|
165
125
|
|
|
166
|
-
|
|
167
|
-
user has granted or the app created. `ContactAccessButton` works well beside a
|
|
168
|
-
search field; `contactAccessPicker(isPresented:completionHandler:)` presents a
|
|
169
|
-
management sheet and returns identifiers for newly granted contacts only.
|
|
170
|
-
|
|
171
|
-
## Multi-Select Contact Picker
|
|
126
|
+
Two ways to widen limited access:
|
|
172
127
|
|
|
173
|
-
|
|
128
|
+
- `ContactAccessButton` fits inline, for example next to a search field, and
|
|
129
|
+
offers matching contacts the app cannot see yet.
|
|
130
|
+
- `contactAccessPicker(isPresented:completionHandler:)` opens a management
|
|
131
|
+
sheet; its completion receives only the identifiers granted in that
|
|
132
|
+
session.
|
|
174
133
|
|
|
175
|
-
|
|
176
|
-
import SwiftUI
|
|
177
|
-
import ContactsUI
|
|
134
|
+
## Selecting several contacts
|
|
178
135
|
|
|
179
|
-
|
|
180
|
-
|
|
136
|
+
Implementing the array variant of the delegate method switches the picker
|
|
137
|
+
into multi-select mode:
|
|
181
138
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return picker
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
func updateUIViewController(_ uiViewController: CNContactPickerViewController, context: Context) {}
|
|
189
|
-
|
|
190
|
-
func makeCoordinator() -> Coordinator {
|
|
191
|
-
Coordinator(self)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
final class Coordinator: NSObject, CNContactPickerDelegate {
|
|
195
|
-
let parent: MultiContactPicker
|
|
196
|
-
|
|
197
|
-
init(_ parent: MultiContactPicker) {
|
|
198
|
-
self.parent = parent
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
func contactPicker(_ picker: CNContactPickerViewController, didSelect contacts: [CNContact]) {
|
|
202
|
-
parent.selectedContacts = contacts
|
|
203
|
-
}
|
|
139
|
+
```swift
|
|
140
|
+
final class GuestListCoordinator: NSObject, CNContactPickerDelegate {
|
|
141
|
+
var onChoose: ([CNContact]) -> Void = { _ in }
|
|
204
142
|
|
|
205
|
-
|
|
143
|
+
func contactPicker(_ picker: CNContactPickerViewController, didSelect contacts: [CNContact]) {
|
|
144
|
+
onChoose(contacts) // the array overload is what turns on multi-select
|
|
206
145
|
}
|
|
207
146
|
}
|
|
208
147
|
```
|
|
209
148
|
|
|
210
|
-
###
|
|
211
|
-
|
|
212
|
-
Configure the picker to only return email addresses.
|
|
149
|
+
### Picking just an email address
|
|
213
150
|
|
|
214
151
|
```swift
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
227
|
-
picker.displayedPropertyKeys = [CNContactEmailAddressesKey]
|
|
228
|
-
return picker
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
func updateUIViewController(_ uiViewController: CNContactPickerViewController, context: Context) {}
|
|
232
|
-
|
|
233
|
-
func makeCoordinator() -> Coordinator {
|
|
234
|
-
Coordinator(self)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
final class Coordinator: NSObject, CNContactPickerDelegate {
|
|
238
|
-
let parent: EmailPicker
|
|
152
|
+
@MainActor
|
|
153
|
+
func makeEmailPicker(delegate: CNContactPickerDelegate) -> CNContactPickerViewController {
|
|
154
|
+
let picker = CNContactPickerViewController()
|
|
155
|
+
picker.delegate = delegate
|
|
156
|
+
let hasEmail = NSPredicate(format: "emailAddresses.@count >= 1")
|
|
157
|
+
let emailRowOnly = NSPredicate(format: "key == %@", CNContactEmailAddressesKey)
|
|
158
|
+
picker.predicateForEnablingContact = hasEmail
|
|
159
|
+
picker.predicateForSelectionOfProperty = emailRowOnly
|
|
160
|
+
picker.displayedPropertyKeys = [CNContactEmailAddressesKey]
|
|
161
|
+
return picker
|
|
162
|
+
}
|
|
239
163
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
164
|
+
final class InviteCoordinator: NSObject, CNContactPickerDelegate {
|
|
165
|
+
var onAddress: (String) -> Void = { _ in }
|
|
243
166
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
) {
|
|
248
|
-
parent.selectedEmail = contactProperty.value as? String
|
|
249
|
-
}
|
|
167
|
+
func contactPicker(_ picker: CNContactPickerViewController,
|
|
168
|
+
didSelect contactProperty: CNContactProperty) {
|
|
169
|
+
if let address = contactProperty.value as? String { onAddress(address) }
|
|
250
170
|
}
|
|
251
171
|
}
|
|
252
172
|
```
|
|
253
173
|
|
|
254
|
-
##
|
|
174
|
+
## Searching
|
|
255
175
|
|
|
256
|
-
###
|
|
176
|
+
### Built-in predicates
|
|
257
177
|
|
|
258
178
|
```swift
|
|
259
|
-
|
|
260
|
-
let
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
let
|
|
264
|
-
|
|
265
|
-
// By phone number
|
|
266
|
-
let phonePredicate = CNContact.predicateForContacts(
|
|
267
|
-
matching: CNPhoneNumber(stringValue: "+1234567890")
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
// By identifiers (batch fetch)
|
|
271
|
-
let idsPredicate = CNContact.predicateForContacts(withIdentifiers: ["id1", "id2", "id3"])
|
|
272
|
-
|
|
273
|
-
// By group
|
|
274
|
-
let groupPredicate = CNContact.predicateForContactsInGroup(withIdentifier: groupId)
|
|
275
|
-
|
|
276
|
-
// By container
|
|
277
|
-
let containerPredicate = CNContact.predicateForContactsInContainer(
|
|
278
|
-
withIdentifier: containerId
|
|
279
|
-
)
|
|
179
|
+
let named = CNContact.predicateForContacts(matchingName: "Lindqvist")
|
|
180
|
+
let byEmail = CNContact.predicateForContacts(matchingEmailAddress: "ops@harbor.example")
|
|
181
|
+
let byPhone = CNContact.predicateForContacts(matching: CNPhoneNumber(stringValue: "+1 415 555 0100"))
|
|
182
|
+
let byIDs = CNContact.predicateForContacts(withIdentifiers: pinnedIDs)
|
|
183
|
+
let inGroup = CNContact.predicateForContactsInGroup(withIdentifier: teamGroupID)
|
|
184
|
+
let inContainer = CNContact.predicateForContactsInContainer(withIdentifier: book.defaultContainerIdentifier())
|
|
280
185
|
```
|
|
281
186
|
|
|
282
|
-
###
|
|
187
|
+
### Filtering in memory
|
|
283
188
|
|
|
284
|
-
|
|
189
|
+
When no predicate fits, enumerate with the needed key and filter yourself:
|
|
285
190
|
|
|
286
191
|
```swift
|
|
287
|
-
func
|
|
288
|
-
let
|
|
192
|
+
func birthdaysThisMonth(in book: CNContactStore) throws -> [CNContact] {
|
|
193
|
+
let month = Calendar.current.component(.month, from: .now)
|
|
194
|
+
let request = CNContactFetchRequest(keysToFetch: [
|
|
289
195
|
CNContactGivenNameKey as CNKeyDescriptor,
|
|
290
|
-
CNContactFamilyNameKey as CNKeyDescriptor,
|
|
291
196
|
CNContactBirthdayKey as CNKeyDescriptor
|
|
292
|
-
]
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
try store.enumerateContacts(with: request) { contact, _ in
|
|
297
|
-
if let birthday = contact.birthday, birthday.month == month {
|
|
298
|
-
contacts.append(contact)
|
|
299
|
-
}
|
|
197
|
+
])
|
|
198
|
+
var matches: [CNContact] = []
|
|
199
|
+
try book.enumerateContacts(with: request) { person, _ in
|
|
200
|
+
if person.birthday?.month == month { matches.append(person) }
|
|
300
201
|
}
|
|
301
|
-
return
|
|
202
|
+
return matches
|
|
302
203
|
}
|
|
303
204
|
```
|
|
304
205
|
|
|
305
|
-
## vCard
|
|
306
|
-
|
|
307
|
-
### Exporting Contacts to vCard
|
|
206
|
+
## vCard
|
|
308
207
|
|
|
309
208
|
```swift
|
|
310
|
-
func
|
|
311
|
-
|
|
209
|
+
func exportCards(_ people: [CNContact], to file: URL) throws {
|
|
210
|
+
let cards = try CNContactVCardSerialization.data(with: people)
|
|
211
|
+
try cards.write(to: file)
|
|
312
212
|
}
|
|
313
213
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
let
|
|
317
|
-
|
|
214
|
+
func importCards(from file: URL, into book: CNContactStore) throws {
|
|
215
|
+
let parsed = try CNContactVCardSerialization.contacts(with: Data(contentsOf: file))
|
|
216
|
+
let batch = CNSaveRequest()
|
|
217
|
+
for card in parsed {
|
|
218
|
+
if let copy = card.mutableCopy() as? CNMutableContact {
|
|
219
|
+
batch.add(copy, toContainerWithIdentifier: nil)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
try book.execute(batch)
|
|
318
223
|
}
|
|
319
224
|
```
|
|
320
225
|
|
|
321
|
-
|
|
226
|
+
## Groups
|
|
322
227
|
|
|
323
228
|
```swift
|
|
324
|
-
|
|
325
|
-
|
|
229
|
+
let allGroups = try book.groups(matching: nil)
|
|
230
|
+
if let crew = allGroups.first(where: { $0.name == "Sailing Crew" }) {
|
|
231
|
+
let members = try book.unifiedContacts(
|
|
232
|
+
matching: CNContact.predicateForContactsInGroup(withIdentifier: crew.identifier),
|
|
233
|
+
keysToFetch: [CNContactGivenNameKey as CNKeyDescriptor]
|
|
234
|
+
)
|
|
235
|
+
print(members.count)
|
|
326
236
|
}
|
|
327
237
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
for contact in contacts {
|
|
334
|
-
guard let mutable = contact.mutableCopy() as? CNMutableContact else { continue }
|
|
335
|
-
saveRequest.add(mutable, toContainerWithIdentifier: nil)
|
|
336
|
-
}
|
|
238
|
+
let club = CNMutableGroup()
|
|
239
|
+
club.name = "Book Club"
|
|
240
|
+
let makeGroup = CNSaveRequest()
|
|
241
|
+
makeGroup.add(club, toContainerWithIdentifier: nil)
|
|
242
|
+
try book.execute(makeGroup)
|
|
337
243
|
|
|
338
|
-
|
|
339
|
-
|
|
244
|
+
let membership = CNSaveRequest()
|
|
245
|
+
membership.addMember(newMember, to: club)
|
|
246
|
+
membership.removeMember(formerMember, from: club)
|
|
247
|
+
try book.execute(membership)
|
|
340
248
|
```
|
|
341
249
|
|
|
342
|
-
##
|
|
250
|
+
## Change notifications and what Swift can do
|
|
343
251
|
|
|
344
|
-
|
|
252
|
+
In Swift, `CNContactStoreDidChange` is the tool: drop cached contacts and
|
|
253
|
+
fetch again whatever the current authorization allows.
|
|
345
254
|
|
|
346
255
|
```swift
|
|
347
|
-
func
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
func fetchContactsInGroup(_ group: CNGroup) throws -> [CNContact] {
|
|
352
|
-
let predicate = CNContact.predicateForContactsInGroup(withIdentifier: group.identifier)
|
|
353
|
-
let keys: [CNKeyDescriptor] = [
|
|
354
|
-
CNContactGivenNameKey as CNKeyDescriptor,
|
|
355
|
-
CNContactFamilyNameKey as CNKeyDescriptor
|
|
356
|
-
]
|
|
357
|
-
return try store.unifiedContacts(matching: predicate, keysToFetch: keys)
|
|
256
|
+
func observeAddressBook(_ onChange: @escaping @Sendable () -> Void) -> NSObjectProtocol {
|
|
257
|
+
NotificationCenter.default.addObserver(forName: .CNContactStoreDidChange,
|
|
258
|
+
object: nil,
|
|
259
|
+
queue: .main) { _ in onChange() }
|
|
358
260
|
}
|
|
359
261
|
```
|
|
360
262
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
```swift
|
|
364
|
-
func createGroup(name: String) throws {
|
|
365
|
-
let group = CNMutableGroup()
|
|
366
|
-
group.name = name
|
|
367
|
-
|
|
368
|
-
let saveRequest = CNSaveRequest()
|
|
369
|
-
saveRequest.add(group, toContainerWithIdentifier: nil)
|
|
370
|
-
try store.execute(saveRequest)
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
func addContactToGroup(contact: CNContact, group: CNGroup) throws {
|
|
374
|
-
let saveRequest = CNSaveRequest()
|
|
375
|
-
saveRequest.addMember(contact, to: group)
|
|
376
|
-
try store.execute(saveRequest)
|
|
377
|
-
}
|
|
263
|
+
Keep the returned token and pass it to `removeObserver(_:)` when done.
|
|
378
264
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
## Change Notifications and Swift Boundaries
|
|
387
|
-
|
|
388
|
-
For Swift-first apps, use `CNContactStoreDidChange` to invalidate caches and
|
|
389
|
-
refetch the contacts your authorization status allows. Do not write Swift code
|
|
390
|
-
that calls `store.enumerateChanges(matching:)`; that method does not exist.
|
|
391
|
-
Apple's change-history fetch entry point is the Objective-C
|
|
392
|
-
`enumeratorForChangeHistoryFetchRequest:error:` selector, while the Swift
|
|
393
|
-
`enumerator(for:)` overlay is marked unavailable. If a product truly needs
|
|
394
|
-
incremental history tokens, isolate that bridge in Objective-C and expose a
|
|
395
|
-
small Swift wrapper; otherwise, refetch on change notifications.
|
|
396
|
-
|
|
397
|
-
### Watching for Real-Time Changes
|
|
398
|
-
|
|
399
|
-
```swift
|
|
400
|
-
func observeChanges(handler: @escaping () -> Void) -> NSObjectProtocol {
|
|
401
|
-
NotificationCenter.default.addObserver(
|
|
402
|
-
forName: .CNContactStoreDidChange,
|
|
403
|
-
object: nil,
|
|
404
|
-
queue: .main
|
|
405
|
-
) { _ in
|
|
406
|
-
handler()
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
```
|
|
265
|
+
There is no `store.enumerateChanges(matching:)` method; code that calls it
|
|
266
|
+
will not compile. Change history is exposed only through the Objective-C
|
|
267
|
+
selector `enumeratorForChangeHistoryFetchRequest:error:`, which the SDK marks
|
|
268
|
+
unavailable in Swift. If the app truly needs incremental history tokens,
|
|
269
|
+
isolate an Objective-C bridge behind a small Swift wrapper. Otherwise,
|
|
270
|
+
refetch when the notification arrives.
|