@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +285 -285
  5. package/package.json +3 -3
  6. package/pipeline/lib/redact.mjs +3 -2
  7. package/pipeline/scripts/_notices.mjs +1 -1
  8. package/pipeline/scripts/gen-skills-index.mjs +13 -1
  9. package/pipeline/scripts/pre-commit-check.sh +4 -0
  10. package/pipeline/skills/.skill-manifest.json +69 -69
  11. package/pipeline/skills/shared/README.md +70 -70
  12. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  13. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  14. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  15. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  16. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  17. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  18. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  19. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  20. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  21. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  22. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  23. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +345 -277
  24. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  25. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +107 -121
  26. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +145 -165
  27. package/pipeline/skills/shared/external/app-store-review/SKILL.md +306 -326
  28. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  29. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  30. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  31. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +335 -360
  32. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  34. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  35. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  36. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  37. package/pipeline/skills/shared/external/authentication/SKILL.md +277 -381
  38. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  39. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +135 -178
  40. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  41. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  42. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  43. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  44. package/pipeline/skills/shared/external/background-processing/SKILL.md +274 -384
  45. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  46. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +173 -321
  47. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  48. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  49. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  50. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  51. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  52. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  53. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  54. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  55. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  56. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +228 -376
  57. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  58. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  59. package/pipeline/skills/shared/external/core-data/SKILL.md +302 -368
  60. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  61. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  62. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  63. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  64. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  65. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  66. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  67. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  68. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  69. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  70. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  71. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  72. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  73. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  74. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  75. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  76. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  77. package/pipeline/skills/shared/external/device-integrity/SKILL.md +236 -353
  78. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  79. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  80. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  81. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  82. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  83. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  84. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  85. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  86. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  87. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  88. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  89. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  90. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  91. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  92. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  93. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  94. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  95. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  96. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  97. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  98. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  99. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  100. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  101. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  102. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  103. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  104. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  105. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  106. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  107. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  108. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  109. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  111. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  112. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  113. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  114. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
  115. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +1 -1
  116. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +8 -7
  117. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
  118. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +5 -2
  119. package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +100 -0
  120. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +45 -26
  121. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
  122. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +12 -5
  123. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +2 -1
  124. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +6 -5
  125. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +44 -18
  126. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +5 -2
  127. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +10 -11
  128. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +4 -33
  129. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +12 -59
  130. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +297 -267
  131. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  132. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  133. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  134. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  135. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  136. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  137. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  138. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  139. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  140. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  141. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  142. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  143. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  144. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  145. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  146. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  147. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  148. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  149. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  150. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  151. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  152. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  153. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  154. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  155. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  156. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  157. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  158. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  159. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  160. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  161. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  162. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  163. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  164. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  165. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  166. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  167. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  168. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  169. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  170. package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
  171. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  172. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  173. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  174. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  175. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  176. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  177. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +302 -241
  178. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  179. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  180. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  181. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  182. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  183. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  184. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  185. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  186. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  187. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  188. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  189. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  190. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  191. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  192. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +304 -351
  193. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  194. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  195. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  196. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  197. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  198. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  199. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  200. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  201. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  202. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  203. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  204. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  205. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  206. package/pipeline/skills/shared/external/swift-security/SKILL.md +183 -162
  207. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  208. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  209. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +411 -476
  210. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  211. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  212. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  213. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  214. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  215. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  216. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +375 -491
  217. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  218. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  219. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  220. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +397 -457
  221. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  222. package/pipeline/skills/shared/external/swift-testing/SKILL.md +191 -175
  223. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  224. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +81 -84
  225. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  226. package/pipeline/skills/shared/external/swiftdata/SKILL.md +394 -256
  227. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  228. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  229. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  230. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  231. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  232. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  233. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  234. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  235. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  236. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  237. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  238. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  239. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  240. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  241. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  242. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  243. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  244. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  245. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  246. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  247. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  248. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  249. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  250. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  251. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +201 -168
  252. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  253. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +134 -133
  254. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  255. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +111 -138
  256. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  257. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  258. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  259. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  260. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  261. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  262. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  263. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  264. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  265. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  266. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  267. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  268. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  269. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  270. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  271. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  272. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  273. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  274. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  275. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  276. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  277. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  278. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  279. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  280. package/pipeline/skills/shared/external/weatherkit/SKILL.md +160 -315
  281. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  282. package/pipeline/skills/shared/external/widgetkit/SKILL.md +224 -288
  283. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +416 -719
  284. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
@@ -1,498 +1,432 @@
1
1
  ---
2
2
  name: core-data
3
- description: "Build, review, or improve Core Data persistence in apps that have not adopted SwiftData. Use when working with NSManagedObject subclasses, NSFetchedResultsController for list-driven UI, NSBatchInsertRequest / NSBatchDeleteRequest / NSBatchUpdateRequest for bulk operations, NSPersistentHistoryChangeRequest for persistent history tracking and multi-target sync, NSStagedMigrationManager for staged schema migrations (iOS 17+), NSCompositeAttributeDescription for composite attributes (iOS 17+), or when integrating Core Data threading with Swift Concurrency. For Core Data + SwiftData coexistence or migration, see the swiftdata skill instead."
3
+ description: "Create, audit or refine Core Data persistence for apps still on Core Data rather than SwiftData: stack setup, NSManagedObject subclasses, NSFetchedResultsController for list UIs, NSBatchInsertRequest, NSBatchDeleteRequest and NSBatchUpdateRequest bulk work, NSPersistentHistoryChangeRequest history tracking across app and extension targets, NSStagedMigrationManager staged migrations and NSCompositeAttributeDescription composite attributes (iOS 17+), and making Core Data contexts work with Swift Concurrency. Use when working on an existing Core Data store. Not for Core Data and SwiftData coexistence or migration; use the swiftdata skill."
4
4
  metadata:
5
- source: "dpearson2699/swift-ios-skills (PolyForm Perimeter 1.0.0)"
5
+ source: multi-agent-pipeline
6
6
  ---
7
7
 
8
8
  # Core Data
9
9
 
10
- Build and maintain data persistence using Core Data for apps that have not
11
- adopted SwiftData. Covers stack setup, concurrency, batch operations,
12
- NSFetchedResultsController, persistent history tracking, staged migration,
13
- and testing.
10
+ For apps that still persist with Core Data rather than SwiftData. Covers the
11
+ stack, queue rules and Swift Concurrency, fetched results controllers, batch
12
+ requests, persistent history, staged migration, composite attributes and
13
+ testing. Hand-offs: SwiftData coexistence and migration go to `swiftdata`;
14
+ iCloud sync details (zones, sharing, `CKError`) go to `cloudkit-sync`; test
15
+ style goes to `swift-testing`.
14
16
 
15
- ## Contents
17
+ ## The stack
16
18
 
17
- - [Stack Setup](#stack-setup)
18
- - [Concurrency and Threading](#concurrency-and-threading)
19
- - [NSFetchedResultsController](#nsfetchedresultscontroller)
20
- - [Batch Operations](#batch-operations)
21
- - [Persistent History Tracking](#persistent-history-tracking)
22
- - [Staged Migration](#staged-migration)
23
- - [Composite Attributes](#composite-attributes)
24
- - [SwiftData Boundary](#swiftdata-boundary)
25
- - [Testing](#testing)
26
- - [Common Mistakes](#common-mistakes)
27
- - [Review Checklist](#review-checklist)
28
- - [References](#references)
29
-
30
- ## Stack Setup
31
-
32
- `NSPersistentContainer` encapsulates the Core Data stack.
33
-
34
- Docs: [NSPersistentContainer](https://sosumi.ai/documentation/coredata/nspersistentcontainer)
19
+ `NSPersistentContainer` wraps the model, coordinator and contexts. Create it
20
+ once and share it.
35
21
 
36
22
  ```swift
37
23
  import CoreData
38
24
 
39
- final class CoreDataStack: @unchecked Sendable {
40
- static let shared = CoreDataStack()
41
-
25
+ final class LibraryStore: Sendable {
26
+ static let shared = LibraryStore()
42
27
  let container: NSPersistentContainer
43
28
 
44
29
  private init() {
45
- container = NSPersistentContainer(name: "MyAppModel")
30
+ container = NSPersistentContainer(name: "Library")
46
31
  container.loadPersistentStores { _, error in
47
- if let error { fatalError("Core Data store failed: \(error)") }
32
+ if let error { fatalError("Store failed to load: \(error)") }
48
33
  }
49
- container.viewContext.automaticallyMergesChangesFromParent = true
50
- container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
34
+ let ui = container.viewContext
35
+ ui.automaticallyMergesChangesFromParent = true
36
+ ui.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump // NSMergeByPropertyObjectTrumpMergePolicy
51
37
  }
52
38
 
53
- var viewContext: NSManagedObjectContext { container.viewContext }
54
-
55
- func newBackgroundContext() -> NSManagedObjectContext {
39
+ func makeWorker() -> NSManagedObjectContext {
56
40
  container.newBackgroundContext()
57
41
  }
58
42
  }
59
43
  ```
60
44
 
61
- For CloudKit sync, use `NSPersistentCloudKitContainer` instead.
45
+ `LibraryStore` is checked `Sendable` with no `@unchecked`: its only stored
46
+ property is a `let` of `NSPersistentContainer`, which the SDK marks `Sendable`.
47
+ Replace `fatalError` with real recovery in shipping code. When the store syncs
48
+ through iCloud, create an `NSPersistentCloudKitContainer` instead.
62
49
 
63
- ## Concurrency and Threading
50
+ ## Queues and threads
64
51
 
65
- Core Data contexts are bound to queues. The `viewContext` is on the main queue;
66
- background contexts operate on private queues.
52
+ Every context belongs to a queue: `viewContext` to the main queue, background
53
+ contexts to private queues.
67
54
 
68
- Docs: [NSManagedObjectContext](https://sosumi.ai/documentation/coredata/nsmanagedobjectcontext)
69
-
70
- **Rules:**
71
- - Always use `perform(_:)` or `performAndWait(_:)` when accessing a context
72
- off its own queue.
73
- - Never pass `NSManagedObject` instances across context or thread boundaries.
74
- Pass `NSManagedObjectID` instead and re-fetch.
75
- - Set `automaticallyMergesChangesFromParent = true` on the `viewContext`.
55
+ - Touch a context from anywhere other than its own queue only inside
56
+ `perform(_:)` or `performAndWait(_:)`.
57
+ - Never hand an `NSManagedObject` to another context or thread. Pass its
58
+ `NSManagedObjectID` and fetch it again on the other side.
59
+ - Turn on `automaticallyMergesChangesFromParent` for `viewContext`.
76
60
 
77
61
  ```swift
78
- // Writing on a background context
79
- func updateTrip(id: NSManagedObjectID, newName: String) async throws {
80
- let context = CoreDataStack.shared.newBackgroundContext()
81
- try await context.perform {
82
- guard let trip = try context.existingObject(with: id) as? CDTrip else {
83
- throw PersistenceError.notFound
84
- }
85
- trip.name = newName
86
- try context.save()
62
+ func markReturned(_ loanID: NSManagedObjectID) async throws {
63
+ let worker = LibraryStore.shared.makeWorker()
64
+ try await worker.perform {
65
+ guard let loan = try worker.existingObject(with: loanID) as? Loan else { return }
66
+ loan.returnedAt = .now
67
+ try worker.save()
87
68
  }
88
69
  }
89
70
  ```
90
71
 
91
- ### Swift Concurrency Integration
72
+ ### Swift Concurrency
92
73
 
93
- `NSManagedObjectContext.perform(_:)` has an `async throws` overload
94
- (iOS 15+). Avoid marking `NSManagedObject` subclasses as `Sendable`.
74
+ Since iOS 15 you can `try await context.perform { }`. Do not mark
75
+ `NSManagedObject` subclasses `Sendable` or `@unchecked Sendable`; they are not
76
+ thread-safe.
95
77
 
96
78
  ```swift
97
- func importItems(_ records: [ItemRecord]) async throws {
98
- let context = CoreDataStack.shared.newBackgroundContext()
99
- try await context.perform {
100
- for record in records {
101
- let item = CDItem(context: context)
102
- item.id = record.id
103
- item.title = record.title
79
+ func importBooks(_ rows: [BookRow]) async throws {
80
+ let worker = LibraryStore.shared.makeWorker()
81
+ try await worker.perform {
82
+ for row in rows {
83
+ let book = Book(context: worker)
84
+ book.isbn = row.isbn
85
+ book.title = row.title
104
86
  }
105
- try context.save()
87
+ if worker.hasChanges { try worker.save() }
106
88
  }
107
- // After save completes, viewContext auto-merges if configured
89
+ // viewContext picks this up through automatic merging
108
90
  }
109
- ```
110
-
111
- **Do not use `@unchecked Sendable` on managed objects.** If you need
112
- cross-boundary communication, pass the `objectID` (which is `Sendable`)
113
- and re-fetch:
114
91
 
115
- ```swift
116
- let objectID = trip.objectID // Sendable
117
- Task.detached {
118
- let bgContext = CoreDataStack.shared.newBackgroundContext()
119
- try await bgContext.perform {
120
- let trip = try bgContext.existingObject(with: objectID) as! CDTrip
121
- trip.isFavorite = true
122
- try bgContext.save()
92
+ func archive(_ bookID: NSManagedObjectID) {
93
+ Task.detached { // NSManagedObjectID is Sendable
94
+ let worker = LibraryStore.shared.makeWorker()
95
+ try await worker.perform {
96
+ let book = try worker.existingObject(with: bookID) as? Book
97
+ book?.isArchived = true
98
+ try worker.save()
99
+ }
123
100
  }
124
101
  }
125
102
  ```
126
103
 
127
- ## NSFetchedResultsController
104
+ ## `NSFetchedResultsController`
128
105
 
129
- Efficiently drives `UITableView` / `UICollectionView` from a Core Data fetch
130
- request, with built-in change tracking and optional caching.
131
-
132
- Docs: [NSFetchedResultsController](https://sosumi.ai/documentation/coredata/nsfetchedresultscontroller)
106
+ An FRC feeds a table or collection view from a fetch request, tracks changes,
107
+ and can cache section data.
133
108
 
134
109
  ```swift
135
- import CoreData
136
110
  import UIKit
137
111
 
138
- class TripsViewController: UITableViewController, NSFetchedResultsControllerDelegate {
139
-
140
- private lazy var fetchedResultsController: NSFetchedResultsController<CDTrip> = {
141
- let request: NSFetchRequest<CDTrip> = CDTrip.fetchRequest()
142
- request.sortDescriptors = [
143
- NSSortDescriptor(keyPath: \CDTrip.startDate, ascending: false)
144
- ]
145
- request.fetchBatchSize = 20
146
-
147
- let controller = NSFetchedResultsController(
148
- fetchRequest: request,
149
- managedObjectContext: CoreDataStack.shared.viewContext,
150
- sectionNameKeyPath: nil,
151
- cacheName: "TripsCache"
152
- )
153
- controller.delegate = self
154
- return controller
155
- }()
112
+ final class ShelfViewController: UICollectionViewController, @preconcurrency NSFetchedResultsControllerDelegate {
113
+ private var results: NSFetchedResultsController<Book>!
114
+ private var dataSource: UICollectionViewDiffableDataSource<String, NSManagedObjectID>!
156
115
 
157
116
  override func viewDidLoad() {
158
117
  super.viewDidLoad()
159
- try? fetchedResultsController.performFetch()
160
- }
161
-
162
- // MARK: - UITableViewDataSource
163
-
164
- override func numberOfSections(in tableView: UITableView) -> Int {
165
- fetchedResultsController.sections?.count ?? 0
118
+ let request = Book.fetchRequest()
119
+ request.sortDescriptors = [NSSortDescriptor(keyPath: \Book.title, ascending: true)]
120
+ request.fetchBatchSize = 25
121
+ results = NSFetchedResultsController(fetchRequest: request,
122
+ managedObjectContext: LibraryStore.shared.container.viewContext,
123
+ sectionNameKeyPath: nil,
124
+ cacheName: nil)
125
+ results.delegate = self
126
+ try? results.performFetch()
166
127
  }
167
128
 
168
- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
169
- fetchedResultsController.sections?[section].numberOfObjects ?? 0
170
- }
171
-
172
- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
173
- let cell = tableView.dequeueReusableCell(withIdentifier: "TripCell", for: indexPath)
174
- let trip = fetchedResultsController.object(at: indexPath)
175
- cell.textLabel?.text = trip.name
176
- return cell
177
- }
178
-
179
- // MARK: - NSFetchedResultsControllerDelegate (diffable)
180
-
181
- func controller(
182
- _ controller: NSFetchedResultsController<any NSFetchRequestResult>,
183
- didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference
184
- ) {
185
- let snapshot = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
186
- dataSource.apply(snapshot, animatingDifferences: true)
129
+ func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
130
+ didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
131
+ let typed = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
132
+ dataSource.apply(typed, animatingDifferences: true)
187
133
  }
188
134
  }
189
135
  ```
190
136
 
191
- **Key points:**
192
- - The fetch request **must** have at least one sort descriptor.
193
- - Call `deleteCache(withName:)` before changing the fetch request predicate or
194
- sort descriptors, or set `cacheName` to `nil`.
195
- - The diffable snapshot delegate method (`didChangeContentWith:`) is available
196
- iOS 13+ and is preferred over the older per-change callbacks.
197
- - After a context `reset()`, call `performFetch()` again.
137
+ Without a diffable data source, read `results.sections?.count`,
138
+ `results.sections?[section].numberOfObjects` and `results.object(at:)`.
198
139
 
199
- ## Batch Operations
140
+ Rules:
200
141
 
201
- Batch operations execute at the SQL level, bypassing the managed object
202
- context. They are fast but don't trigger context notifications automatically.
142
+ - The fetch request needs at least one sort descriptor.
143
+ - Before changing the predicate or sort descriptors, call
144
+ `NSFetchedResultsController.deleteCache(withName:)` or use `cacheName: nil`.
145
+ - The snapshot callback (iOS 13+) is preferred over the per-object change callbacks.
146
+ - After `reset()` on the context, call `performFetch()` again.
203
147
 
204
- ### NSBatchInsertRequest (iOS 13+)
148
+ ## Batch requests
205
149
 
206
- Docs: [NSBatchInsertRequest](https://sosumi.ai/documentation/coredata/nsbatchinsertrequest)
150
+ Batch requests run directly against SQLite. They skip the context, which makes
151
+ them fast, but they post no context notifications, so **merge their results
152
+ yourself**.
207
153
 
208
154
  ```swift
209
- func batchImport(_ records: [[String: Any]]) async throws {
210
- let context = CoreDataStack.shared.newBackgroundContext()
211
- try await context.perform {
212
- let request = NSBatchInsertRequest(
213
- entity: CDTrip.entity(),
214
- objects: records
215
- )
216
- request.resultType = .objectIDs
217
- let result = try context.execute(request) as? NSBatchInsertResult
218
- if let ids = result?.result as? [NSManagedObjectID] {
219
- NSManagedObjectContext.mergeChanges(
220
- fromRemoteContextSave: [NSInsertedObjectsKey: ids],
221
- into: [CoreDataStack.shared.viewContext]
222
- )
223
- }
224
- }
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])
225
159
  }
226
- ```
227
-
228
- ### NSBatchDeleteRequest (iOS 9+)
229
160
 
230
- Docs: [NSBatchDeleteRequest](https://sosumi.ai/documentation/coredata/nsbatchdeleterequest)
161
+ let worker = LibraryStore.shared.makeWorker()
231
162
 
232
- ```swift
233
- func deleteOldTrips(before cutoff: Date) async throws {
234
- let context = CoreDataStack.shared.newBackgroundContext()
235
- try await context.perform {
236
- let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CDTrip.fetchRequest()
237
- fetchRequest.predicate = NSPredicate(format: "endDate < %@", cutoff as NSDate)
238
- let request = NSBatchDeleteRequest(fetchRequest: fetchRequest)
239
- request.resultType = .resultTypeObjectIDs
240
- let result = try context.execute(request) as? NSBatchDeleteResult
241
- if let ids = result?.result as? [NSManagedObjectID] {
242
- NSManagedObjectContext.mergeChanges(
243
- fromRemoteContextSave: [NSDeletedObjectsKey: ids],
244
- into: [CoreDataStack.shared.viewContext]
245
- )
246
- }
247
- }
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)
248
169
  }
249
- ```
250
170
 
251
- ### NSBatchUpdateRequest (iOS 8+)
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
+ }
252
180
 
253
- ```swift
254
- func markAllTripsAsNotFavorite() async throws {
255
- let context = CoreDataStack.shared.newBackgroundContext()
256
- try await context.perform {
257
- let request = NSBatchUpdateRequest(entity: CDTrip.entity())
258
- request.propertiesToUpdate = ["isFavorite": false]
259
- request.resultType = .updatedObjectIDsResultType
260
- let result = try context.execute(request) as? NSBatchUpdateResult
261
- if let ids = result?.result as? [NSManagedObjectID] {
262
- NSManagedObjectContext.mergeChanges(
263
- fromRemoteContextSave: [NSUpdatedObjectsKey: ids],
264
- into: [CoreDataStack.shared.viewContext]
265
- )
266
- }
267
- }
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)
268
188
  }
269
189
  ```
270
190
 
271
- **Always merge changes** back into relevant contexts after batch operations.
272
- Batch delete does not enforce the Deny delete rule.
273
-
274
- ## Persistent History Tracking
191
+ A batch delete does not honor the **Deny** delete rule and skips validation
192
+ beyond the basic delete rules. Check those constraints yourself first.
275
193
 
276
- Track store-level changes across targets (app, extensions, widgets) and
277
- processes.
194
+ ## Persistent history
278
195
 
279
- Docs: [NSPersistentHistoryChangeRequest](https://sosumi.ai/documentation/coredata/nspersistenthistorychangerequest)
196
+ History tracking records store-level changes so the app, its extensions and
197
+ widgets (separate processes) can see each other's writes, including batch
198
+ requests.
280
199
 
281
- ### Enable History Tracking
200
+ ### Turn it on
282
201
 
283
202
  ```swift
284
- let description = NSPersistentStoreDescription()
285
- description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
286
- description.setOption(true as NSNumber,
287
- forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
203
+ guard let description = container.persistentStoreDescriptions.first else { return }
204
+ for key in [NSPersistentHistoryTrackingKey, NSPersistentStoreRemoteChangeNotificationPostOptionKey] {
205
+ description.setOption(NSNumber(value: true), forKey: key)
206
+ }
288
207
  container.persistentStoreDescriptions = [description]
289
208
  ```
290
209
 
291
- ### Observe, Fetch, Merge, and Purge
210
+ ### Consume it
211
+
212
+ 1. Observe `.NSPersistentStoreRemoteChange`, with the container's persistent store coordinator as the object.
213
+ 2. On a background context, fetch transactions after your last token.
214
+ 3. Merge each transaction into `viewContext` and move the token forward.
215
+ 4. Purge history older than the token.
292
216
 
293
217
  ```swift
294
- // 1. Observe remote change notifications
295
- NotificationCenter.default.addObserver(
296
- self, selector: #selector(storeRemoteChange(_:)),
297
- name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator
298
- )
299
-
300
- // 2. Fetch history since last token
301
- @objc func storeRemoteChange(_ notification: Notification) {
302
- let context = container.newBackgroundContext()
303
- context.perform {
304
- let request = NSPersistentHistoryChangeRequest.fetchHistory(after: self.lastToken)
305
- if let result = try? context.execute(request) as? NSPersistentHistoryResult,
306
- let transactions = result.result as? [NSPersistentHistoryTransaction] {
307
- // 3. Merge into viewContext
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
308
241
  for transaction in transactions {
309
- self.container.viewContext.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
310
- self.lastToken = transaction.token
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))
311
249
  }
312
250
  }
313
- // 4. Purge old history
314
- let purgeRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: self.lastToken)
315
- try? context.execute(purgeRequest)
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)
316
265
  }
317
266
  }
318
267
  ```
319
268
 
320
- Store `lastToken` in `UserDefaults` (per target) so history is processed
321
- correctly across launches.
269
+ `@unchecked Sendable` rests on one guarantee: after `init` (which sets
270
+ `lastToken` before the observer is registered), `lastToken` is read and written
271
+ only inside `worker.perform`, and one context runs its `perform` blocks one at a
272
+ time on its private queue. Creating a new background context per notification
273
+ would break that, because two notifications could then run on two queues at
274
+ once.
322
275
 
323
- ## Staged Migration
276
+ The static merge call is safe from the worker queue; calling
277
+ `viewContext.mergeChanges(fromContextDidSave: transaction.objectIDNotification())`
278
+ inside `viewContext.perform` is the equivalent instance form.
324
279
 
325
- `NSStagedMigrationManager` (iOS 17+) sequences schema migrations through
326
- ordered stages, each lightweight or custom.
280
+ Keep one token per target (for example in the shared `UserDefaults` suite) so
281
+ each process resumes where it stopped after relaunch. Purge only up to the
282
+ point every reader has consumed.
327
283
 
328
- Docs: [NSStagedMigrationManager](https://sosumi.ai/documentation/coredata/nsstagedmigrationmanager)
284
+ ## Staged migration (iOS 17+)
329
285
 
330
- ```swift
331
- import CoreData
286
+ `NSStagedMigrationManager` applies an ordered list of stages, each either
287
+ lightweight or custom.
288
+
289
+ - `NSLightweightMigrationStage` takes the **version checksums** (`[String]`) of
290
+ compiled model versions, not their names, and has a `label`.
291
+ - `NSManagedObjectModelReference(name:in:versionChecksum:)` points to one model version.
292
+ - `NSCustomMigrationStage(migratingFrom:to:)` takes a source and a destination
293
+ reference and offers `label` and `willMigrateHandler` (called with the manager and stage).
332
294
 
333
- // Define migration stages
334
- // Use version checksums from the compiled model versions, not model names.
335
- let checksumV1 = "<ModelV1 version checksum>"
336
- let checksumV2 = "<ModelV2 version checksum>"
337
- let checksumV3 = "<ModelV3 version checksum>"
338
- let stage1to2 = NSLightweightMigrationStage([checksumV1, checksumV2])
339
- stage1to2.label = "Add isFavorite property"
340
-
341
- let modelV2 = NSManagedObjectModelReference(
342
- name: "ModelV2",
343
- in: Bundle.main,
344
- versionChecksum: checksumV2
345
- )
346
- let modelV3 = NSManagedObjectModelReference(
347
- name: "ModelV3",
348
- in: Bundle.main,
349
- versionChecksum: checksumV3
350
- )
351
- let stage2to3 = NSCustomMigrationStage(
352
- migratingFrom: modelV2,
353
- to: modelV3
354
- )
355
- stage2to3.label = "Split name into firstName/lastName"
356
- stage2to3.willMigrateHandler = { migrationManager, currentStage in
357
- guard let container = migrationManager.container else { return }
358
- let context = container.newBackgroundContext()
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()
359
305
  try context.performAndWait {
360
- // Transform data between schema versions
361
- let request = NSFetchRequest<NSManagedObject>(entityName: "Person")
362
- let people = try context.fetch(request)
363
- for person in people {
364
- let fullName = person.value(forKey: "name") as? String ?? ""
365
- let parts = fullName.split(separator: " ", maxSplits: 1)
366
- person.setValue(String(parts.first ?? ""), forKey: "firstName")
367
- person.setValue(parts.count > 1 ? String(parts.last!) : "", forKey: "lastName")
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")
368
312
  }
369
313
  try context.save()
370
314
  }
371
315
  }
372
316
 
373
- // Apply to the persistent store
374
- let manager = NSStagedMigrationManager([stage1to2, stage2to3])
375
- let description = NSPersistentStoreDescription()
376
- description.setOption(manager,
377
- forKey: NSPersistentStoreStagedMigrationManagerOptionKey)
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)
378
323
  container.persistentStoreDescriptions = [description]
379
324
  container.loadPersistentStores { _, error in
380
- if let error { fatalError("Migration failed: \(error)") }
325
+ if let error { MigrationLog.failed(error) }
381
326
  }
382
327
  ```
383
328
 
384
- For apps targeting below iOS 17, use lightweight migration
385
- (`NSInferMappingModelAutomaticallyOption`) or mapping models.
386
-
387
- `NSLightweightMigrationStage` takes **version checksums** (`[String]`), not
388
- human-readable model names.
389
-
390
- ## Composite Attributes
391
-
392
- iOS 17+ supports composite attributes: groups of sub-attributes on an entity
393
- that act as a single logical unit. Define them in the model editor by adding a
394
- Composite type attribute and nesting sub-attributes beneath it.
329
+ The option must be on the store description **before** `loadPersistentStores`.
330
+ Read the checksums from the compiled `.momd` (for example
331
+ `NSManagedObjectModel.versionChecksum`), never type a model name in their place.
395
332
 
396
- Docs: [NSCompositeAttributeDescription](https://sosumi.ai/documentation/coredata/nscompositeattributedescription)
333
+ Older deployment targets fall back to inferred mapping
334
+ (`NSInferMappingModelAutomaticallyOption`) or hand-written mapping models.
397
335
 
398
- Composite attributes map to `Codable` structs in SwiftData coexistence
399
- scenarios.
336
+ ## Composite attributes (iOS 17+)
400
337
 
401
- ## SwiftData Boundary
338
+ `NSCompositeAttributeDescription` groups several sub-attributes into one
339
+ logical attribute, for example an address made of street, city and postcode.
340
+ Create it in the model editor by choosing the **Composite** type and adding
341
+ nested attributes. When the same store is read by SwiftData, a composite maps
342
+ to a `Codable` struct.
402
343
 
403
- Use the `swiftdata` skill for Core Data + SwiftData coexistence or migration
404
- implementation. Before handing off, preserve these Core Data boundaries:
344
+ ## Where SwiftData starts
405
345
 
406
- - SwiftData must point at the existing persistent store URL when it is meant to
407
- share or migrate Core Data data.
408
- - Shared persisted data must keep entity names, property names, types, and
409
- schema compatible across the Core Data model and SwiftData `@Model` classes.
410
- - Map renamed persisted properties with SwiftData `@Attribute(originalName:)`.
346
+ Coexistence and migration work belongs to `swiftdata`. The rules that matter
347
+ from this side:
411
348
 
412
- ## Testing
349
+ - Point SwiftData at the **existing** store URL when it shares or takes over Core Data data.
350
+ - The `@Model` classes must line up with the Core Data model: same entity and
351
+ property names, matching types, a compatible schema.
352
+ - Map renamed persisted properties with `@Attribute(originalName:)`.
413
353
 
414
- ### In-Memory Store for Tests
354
+ ## Testing with an in-memory store
415
355
 
416
356
  ```swift
417
- import CoreData
418
357
  import Testing
358
+ import CoreData
419
359
 
420
- struct CoreDataTests {
421
- func makeTestContainer() throws -> NSPersistentContainer {
422
- let container = NSPersistentContainer(name: "MyAppModel")
423
- let description = NSPersistentStoreDescription()
424
- description.type = NSInMemoryStoreType
425
- container.persistentStoreDescriptions = [description]
426
-
427
- var loadError: Error?
428
- container.loadPersistentStores { _, error in loadError = error }
429
- if let loadError { throw loadError }
430
- return container
431
- }
432
-
433
- @Test func createAndFetchTrip() throws {
434
- let container = try makeTestContainer()
435
- let context = container.viewContext
436
-
437
- let trip = CDTrip(context: context)
438
- trip.name = "Test Trip"
439
- trip.startDate = .now
440
- try context.save()
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
+ }
441
366
 
442
- let request: NSFetchRequest<CDTrip> = CDTrip.fetchRequest()
443
- let trips = try context.fetch(request)
444
- #expect(trips.count == 1)
445
- #expect(trips.first?.name == "Test Trip")
446
- }
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
447
376
  }
448
- ```
449
377
 
450
- **Tips:**
451
- - Share the `NSManagedObjectModel` instance across tests to avoid "duplicate
452
- entity" warnings.
453
- - Use a single shared model loaded once:
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()
454
383
 
455
- ```swift
456
- private let sharedModel: NSManagedObjectModel = {
457
- let url = Bundle.main.url(forResource: "MyAppModel", withExtension: "momd")!
458
- return NSManagedObjectModel(contentsOf: url)!
459
- }()
460
-
461
- func makeTestContainer() throws -> NSPersistentContainer {
462
- let container = NSPersistentContainer(name: "MyAppModel",
463
- managedObjectModel: sharedModel)
464
- // ... configure in-memory store
384
+ let fetched = try context.fetch(Book.fetchRequest())
385
+ #expect(fetched.count == 1)
386
+ #expect(fetched.first?.title == "Dune")
465
387
  }
466
388
  ```
467
389
 
468
- ## Common Mistakes
390
+ Load the model once and pass the same `NSManagedObjectModel` to every test
391
+ container; loading it per test produces "multiple entity descriptions claim"
392
+ duplicate-entity warnings.
393
+
394
+ ## Common mistakes
469
395
 
470
396
  | Mistake | Fix |
471
- |---------|-----|
472
- | Passing `NSManagedObject` across threads | Pass `objectID` and re-fetch in the target context |
473
- | Forgetting to merge batch operation results | Call `mergeChanges(fromRemoteContextSave:into:)` |
474
- | Calling `save()` without checking `hasChanges` | Guard with `context.hasChanges` first |
475
- | Using deprecated `init(concurrencyType:)` confinement type | Use `.privateQueueConcurrencyType` or `.mainQueueConcurrencyType` |
476
- | Not setting `mergePolicy` on `viewContext` | Set `NSMergeByPropertyObjectTrumpMergePolicy` to avoid conflict crashes |
477
- | Modifying fetch request on live `NSFetchedResultsController` without deleting cache | Call `deleteCache(withName:)` first or use `cacheName: nil` |
478
- | Batch delete ignoring Deny delete rule | Batch delete bypasses delete rules; validate manually |
479
- | Marking `NSManagedObject` as `@unchecked Sendable` | Do not. Pass `objectID` instead |
480
- | Pointing SwiftData at a fresh store during coexistence | Use the existing store URL and compatible schema when SwiftData should share or migrate Core Data data |
481
-
482
- ## Review Checklist
483
-
484
- - [ ] `NSPersistentContainer` is initialized once and shared
485
- - [ ] `viewContext` used only on main queue; background contexts for writes
486
- - [ ] `perform(_:)` or `performAndWait(_:)` wraps all off-queue context access
487
- - [ ] `automaticallyMergesChangesFromParent` set on `viewContext`
488
- - [ ] `mergePolicy` set on `viewContext` to prevent conflict crashes
489
- - [ ] Batch operation results merged into relevant contexts
490
- - [ ] `NSFetchedResultsController` fetch requests have sort descriptors
491
- - [ ] Persistent history tracking enabled for multi-target apps
492
- - [ ] Core Data + SwiftData handoff preserves store URL, schema compatibility, entity/property names, and rename mappings
493
- - [ ] Tests use in-memory stores with shared `NSManagedObjectModel`
494
- - [ ] No `NSManagedObject` instances cross thread boundaries
397
+ |---|---|
398
+ | Handing an `NSManagedObject` to another thread | Send `objectID` and fetch again |
399
+ | Batch results never merged | `NSManagedObjectContext.mergeChanges(fromRemoteContextSave:into:)` |
400
+ | `save()` with nothing to save | Check `context.hasChanges` first |
401
+ | The deprecated confinement type passed to `init(concurrencyType:)` | Pick `.privateQueueConcurrencyType` or `.mainQueueConcurrencyType` |
402
+ | No `mergePolicy` on `viewContext` | `NSMergeByPropertyObjectTrumpMergePolicy`, avoiding merge-conflict failures |
403
+ | Editing a live FRC request without clearing its cache | `deleteCache(withName:)` or `cacheName: nil` |
404
+ | Relying on the Deny rule with batch delete | Batch delete ignores it; validate manually |
405
+ | `@unchecked Sendable` on managed objects | Remove it; pass `objectID` |
406
+ | SwiftData opening a fresh, empty file while sharing data with Core Data | Reuse the current store URL and keep the schema compatible |
407
+
408
+ ## Review checklist
409
+
410
+ - [ ] One `NSPersistentContainer`, created once and shared
411
+ - [ ] `viewContext` used only on the main queue; writes on background contexts
412
+ - [ ] Every off-queue access wrapped in `perform` or `performAndWait`
413
+ - [ ] `automaticallyMergesChangesFromParent` on `viewContext`
414
+ - [ ] `mergePolicy` on `viewContext`
415
+ - [ ] Batch request results merged
416
+ - [ ] FRC fetch requests sorted
417
+ - [ ] Persistent history enabled when several targets share the store
418
+ - [ ] SwiftData hand-off keeps the store URL, schema, entity and property names, and rename mappings
419
+ - [ ] Test containers are in-memory and share a single loaded model
420
+ - [ ] No `NSManagedObject` crosses a thread boundary
495
421
 
496
422
  ## References
497
423
 
498
- - Apple docs: [Core Data](https://sosumi.ai/documentation/coredata) | [NSPersistentContainer](https://sosumi.ai/documentation/coredata/nspersistentcontainer) | [NSFetchedResultsController](https://sosumi.ai/documentation/coredata/nsfetchedresultscontroller) | [NSStagedMigrationManager](https://sosumi.ai/documentation/coredata/nsstagedmigrationmanager)
424
+ - [Core Data](https://developer.apple.com/documentation/coredata)
425
+ - [NSPersistentContainer](https://developer.apple.com/documentation/coredata/nspersistentcontainer)
426
+ - [NSManagedObjectContext](https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext)
427
+ - [NSFetchedResultsController](https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller)
428
+ - [NSBatchInsertRequest](https://developer.apple.com/documentation/coredata/nsbatchinsertrequest)
429
+ - [NSBatchDeleteRequest](https://developer.apple.com/documentation/coredata/nsbatchdeleterequest)
430
+ - [NSPersistentHistoryChangeRequest](https://developer.apple.com/documentation/coredata/nspersistenthistorychangerequest)
431
+ - [NSStagedMigrationManager](https://developer.apple.com/documentation/coredata/nsstagedmigrationmanager)
432
+ - [NSCompositeAttributeDescription](https://developer.apple.com/documentation/coredata/nscompositeattributedescription)