@devo-bmad-custom/agent-orchestration 1.0.1 → 1.0.3
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/lib/installer.js +44 -11
- package/package.json +1 -1
- package/src/.agents/skills/audit-website/README.md +20 -20
- package/src/.agents/skills/audit-website/SKILL.md +470 -470
- package/src/.agents/skills/audit-website/agents/openai.yaml +6 -6
- package/src/.agents/skills/audit-website/assets/icon-small.svg +41 -41
- package/src/.agents/skills/audit-website/references/OUTPUT-FORMAT.md +250 -250
- package/src/.agents/skills/clean-code-standards/SKILL.md +104 -104
- package/src/.agents/skills/excalidraw-dark-standard/SKILL.md +281 -281
- package/src/.agents/skills/frontend-responsive-design-standards/SKILL.md +434 -434
- package/src/.agents/skills/java-fundamentals/SKILL.md +116 -116
- package/src/.agents/skills/java-performance/SKILL.md +119 -119
- package/src/.agents/skills/next-best-practices/SKILL.md +153 -153
- package/src/.agents/skills/next-best-practices/async-patterns.md +87 -87
- package/src/.agents/skills/next-best-practices/bundling.md +180 -180
- package/src/.agents/skills/next-best-practices/data-patterns.md +297 -297
- package/src/.agents/skills/next-best-practices/debug-tricks.md +105 -105
- package/src/.agents/skills/next-best-practices/directives.md +73 -73
- package/src/.agents/skills/next-best-practices/error-handling.md +227 -227
- package/src/.agents/skills/next-best-practices/file-conventions.md +140 -140
- package/src/.agents/skills/next-best-practices/font.md +245 -245
- package/src/.agents/skills/next-best-practices/functions.md +108 -108
- package/src/.agents/skills/next-best-practices/hydration-error.md +91 -91
- package/src/.agents/skills/next-best-practices/image.md +173 -173
- package/src/.agents/skills/next-best-practices/metadata.md +301 -301
- package/src/.agents/skills/next-best-practices/parallel-routes.md +287 -287
- package/src/.agents/skills/next-best-practices/route-handlers.md +146 -146
- package/src/.agents/skills/next-best-practices/rsc-boundaries.md +159 -159
- package/src/.agents/skills/next-best-practices/runtime-selection.md +39 -39
- package/src/.agents/skills/next-best-practices/scripts.md +141 -141
- package/src/.agents/skills/next-best-practices/self-hosting.md +371 -371
- package/src/.agents/skills/next-best-practices/suspense-boundaries.md +67 -67
- package/src/.agents/skills/nextjs-app-router-patterns/SKILL.md +537 -537
- package/src/.agents/skills/postgresql-optimization/SKILL.md +404 -404
- package/src/.agents/skills/python-backend/SKILL.md +153 -153
- package/src/.agents/skills/python-fundamentals/SKILL.md +234 -234
- package/src/.agents/skills/python-performance/SKILL.md +404 -404
- package/src/.agents/skills/react-expert/SKILL.md +335 -335
- package/src/.agents/skills/redis-best-practices/SKILL.md +438 -438
- package/src/.agents/skills/security-best-practices/SKILL.md +288 -288
- package/src/.agents/skills/security-review/LICENSE +22 -22
- package/src/.agents/skills/security-review/SKILL.md +312 -312
- package/src/.agents/skills/security-review/infrastructure/docker.md +432 -432
- package/src/.agents/skills/security-review/languages/javascript.md +388 -388
- package/src/.agents/skills/security-review/languages/python.md +363 -363
- package/src/.agents/skills/security-review/references/api-security.md +519 -519
- package/src/.agents/skills/security-review/references/authentication.md +353 -353
- package/src/.agents/skills/security-review/references/authorization.md +372 -372
- package/src/.agents/skills/security-review/references/business-logic.md +443 -443
- package/src/.agents/skills/security-review/references/cryptography.md +329 -329
- package/src/.agents/skills/security-review/references/csrf.md +398 -398
- package/src/.agents/skills/security-review/references/data-protection.md +378 -378
- package/src/.agents/skills/security-review/references/deserialization.md +410 -410
- package/src/.agents/skills/security-review/references/error-handling.md +436 -436
- package/src/.agents/skills/security-review/references/file-security.md +457 -457
- package/src/.agents/skills/security-review/references/injection.md +259 -259
- package/src/.agents/skills/security-review/references/logging.md +433 -433
- package/src/.agents/skills/security-review/references/misconfiguration.md +435 -435
- package/src/.agents/skills/security-review/references/modern-threats.md +475 -475
- package/src/.agents/skills/security-review/references/ssrf.md +415 -415
- package/src/.agents/skills/security-review/references/supply-chain.md +405 -405
- package/src/.agents/skills/security-review/references/xss.md +336 -336
- package/src/.agents/skills/subagent-driven-development/SKILL.md +275 -275
- package/src/.agents/skills/subagent-driven-development/code-quality-reviewer-prompt.md +26 -26
- package/src/.agents/skills/subagent-driven-development/implementer-prompt.md +113 -113
- package/src/.agents/skills/subagent-driven-development/spec-reviewer-prompt.md +61 -61
- package/src/.agents/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/src/.agents/skills/systematic-debugging/SKILL.md +296 -296
- package/src/.agents/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/src/.agents/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/src/.agents/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/src/.agents/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/src/.agents/skills/systematic-debugging/test-academic.md +14 -14
- package/src/.agents/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/src/.agents/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/src/.agents/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/src/.agents/skills/typescript-best-practices/SKILL.md +373 -373
- package/src/.agents/skills/ui-ux-pro-custom/SKILL.md +348 -348
- package/src/.agents/skills/ui-ux-pro-custom/data/charts.csv +26 -26
- package/src/.agents/skills/ui-ux-pro-custom/data/colors.csv +97 -97
- package/src/.agents/skills/ui-ux-pro-custom/data/icons.csv +101 -101
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/SKILL.md +106 -106
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/accessibility.md +475 -475
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/animation.md +466 -466
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/composition-locals.md +231 -231
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/deprecated-patterns.md +323 -323
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/lists-scrolling.md +400 -400
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/modifiers.md +331 -331
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/navigation.md +416 -416
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/performance.md +446 -446
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/side-effects.md +516 -516
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/source-code/foundation-source.md +13327 -13327
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/source-code/material3-source.md +19097 -19097
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/source-code/navigation-source.md +2947 -2947
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/source-code/runtime-source.md +11316 -11316
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/source-code/ui-source.md +7896 -7896
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/state-management.md +377 -377
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/styles-experimental.md +470 -470
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/theming-material3.md +349 -349
- package/src/.agents/skills/ui-ux-pro-custom/data/jetpack-compose-expert-skill/references/view-composition.md +595 -595
- package/src/.agents/skills/ui-ux-pro-custom/data/landing.csv +31 -31
- package/src/.agents/skills/ui-ux-pro-custom/data/mobile-ui-layout.md +654 -654
- package/src/.agents/skills/ui-ux-pro-custom/data/products.csv +96 -96
- package/src/.agents/skills/ui-ux-pro-custom/data/react-performance.csv +45 -45
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/astro.csv +54 -54
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/flutter.csv +53 -53
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/html-tailwind.csv +56 -56
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/jetpack-compose.csv +53 -53
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/nextjs.csv +53 -53
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/nuxt-ui.csv +51 -51
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/nuxtjs.csv +59 -59
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/react-native.csv +56 -56
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/react.csv +54 -54
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/shadcn.csv +61 -61
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/svelte.csv +54 -54
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/swiftui.csv +51 -51
- package/src/.agents/skills/ui-ux-pro-custom/data/stacks/vue.csv +50 -50
- package/src/.agents/skills/ui-ux-pro-custom/data/styles.csv +68 -68
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/alarmkit/SKILL.md +438 -438
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/alarmkit/references/alarmkit-patterns.md +584 -584
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/app-clips/SKILL.md +436 -436
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/app-intents/SKILL.md +489 -489
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/app-intents/references/appintents-advanced.md +1076 -1076
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/app-store-review/SKILL.md +340 -340
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/app-store-review/references/privacy-manifest.md +90 -90
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/app-store-review/references/review-checklists.md +106 -106
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/apple-on-device-ai/SKILL.md +500 -500
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/apple-on-device-ai/references/coreml-conversion.md +425 -425
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/apple-on-device-ai/references/coreml-optimization.md +344 -344
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/apple-on-device-ai/references/foundation-models.md +508 -508
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/apple-on-device-ai/references/mlx-swift.md +285 -285
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/authentication/SKILL.md +496 -496
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/authentication/references/keychain-biometric.md +211 -211
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/background-processing/SKILL.md +499 -499
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/background-processing/references/background-task-patterns.md +390 -390
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/callkit-voip/SKILL.md +461 -461
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/callkit-voip/references/callkit-patterns.md +425 -425
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/cloudkit-sync/SKILL.md +492 -492
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/cloudkit-sync/references/cloudkit-patterns.md +461 -461
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/codable-patterns/SKILL.md +467 -467
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/contacts-framework/SKILL.md +425 -425
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/contacts-framework/references/contacts-patterns.md +409 -409
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/core-bluetooth/SKILL.md +491 -491
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/core-bluetooth/references/ble-patterns.md +435 -435
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/core-motion/SKILL.md +388 -388
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/core-motion/references/motion-patterns.md +405 -405
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/core-nfc/SKILL.md +495 -495
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/core-nfc/references/nfc-patterns.md +420 -420
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/coreml/SKILL.md +459 -459
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/coreml/references/coreml-swift-integration.md +765 -765
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/debugging-instruments/SKILL.md +422 -422
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/debugging-instruments/references/instruments-guide.md +387 -387
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/debugging-instruments/references/lldb-patterns.md +298 -298
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/device-integrity/SKILL.md +477 -477
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/energykit/SKILL.md +460 -460
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/energykit/references/energykit-patterns.md +541 -541
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/eventkit-calendar/SKILL.md +483 -483
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/eventkit-calendar/references/eventkit-patterns.md +326 -326
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/healthkit/SKILL.md +498 -498
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/healthkit/references/healthkit-patterns.md +602 -602
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/homekit-matter/SKILL.md +496 -496
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/homekit-matter/references/matter-commissioning.md +455 -455
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-accessibility/SKILL.md +301 -301
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-accessibility/references/a11y-patterns.md +140 -140
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-localization/SKILL.md +418 -418
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-localization/references/formatstyle-locale.md +627 -627
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-localization/references/string-catalogs.md +462 -462
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-networking/SKILL.md +441 -441
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-networking/references/background-websocket.md +862 -862
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-networking/references/lightweight-clients.md +93 -93
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-networking/references/network-framework.md +563 -563
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-networking/references/urlsession-patterns.md +1116 -1116
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-security/SKILL.md +496 -496
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-security/references/app-review-guidelines.md +174 -174
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-security/references/cryptokit-advanced.md +296 -296
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-security/references/file-storage-patterns.md +354 -354
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/ios-security/references/privacy-manifest.md +117 -117
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/live-activities/SKILL.md +500 -500
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/live-activities/references/live-activity-patterns.md +868 -868
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/mapkit-location/SKILL.md +485 -485
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/mapkit-location/references/corelocation-patterns.md +730 -730
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/mapkit-location/references/mapkit-patterns.md +748 -748
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/metrickit-diagnostics/SKILL.md +479 -479
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/musickit-audio/SKILL.md +395 -395
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/musickit-audio/references/musickit-patterns.md +363 -363
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/natural-language/SKILL.md +412 -412
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/natural-language/references/translation-patterns.md +311 -311
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/passkit-wallet/SKILL.md +398 -398
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/passkit-wallet/references/wallet-passes.md +254 -254
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/pencilkit-drawing/SKILL.md +387 -387
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/pencilkit-drawing/references/paperkit-integration.md +376 -376
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/pencilkit-drawing/references/pencilkit-patterns.md +302 -302
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/permissionkit/SKILL.md +446 -446
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/permissionkit/references/permissionkit-patterns.md +435 -435
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/photos-camera-media/SKILL.md +500 -500
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/photos-camera-media/references/av-playback.md +701 -701
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/photos-camera-media/references/camera-capture.md +774 -774
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/photos-camera-media/references/image-loading-caching.md +869 -869
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/photos-camera-media/references/photospicker-patterns.md +597 -597
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/push-notifications/SKILL.md +500 -500
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/push-notifications/references/notification-patterns.md +677 -677
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/push-notifications/references/rich-notifications.md +745 -745
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/realitykit-ar/SKILL.md +479 -479
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/realitykit-ar/references/realitykit-patterns.md +480 -480
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/shareplay-activities/SKILL.md +483 -483
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/shareplay-activities/references/shareplay-patterns.md +544 -544
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/speech-recognition/SKILL.md +485 -485
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/storekit/SKILL.md +478 -478
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/storekit/references/app-review-guidelines.md +58 -58
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/storekit/references/storekit-advanced.md +755 -755
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-charts/SKILL.md +487 -487
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-charts/references/charts-patterns.md +895 -895
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-concurrency/SKILL.md +408 -408
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-concurrency/references/approachable-concurrency.md +80 -80
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-concurrency/references/swift-6-2-concurrency.md +233 -233
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-concurrency/references/swiftui-concurrency.md +187 -187
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-concurrency/references/synchronization-primitives.md +341 -341
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-language/SKILL.md +498 -498
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-language/references/swift-patterns-extended.md +505 -505
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-testing/SKILL.md +467 -467
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swift-testing/references/testing-patterns.md +504 -504
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftdata/SKILL.md +334 -334
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftdata/references/core-data-coexistence.md +504 -504
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftdata/references/swiftdata-advanced.md +975 -975
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftdata/references/swiftdata-queries.md +675 -675
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-animation/SKILL.md +481 -481
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-animation/references/animation-advanced.md +804 -804
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-animation/references/core-animation-bridge.md +553 -553
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-gestures/SKILL.md +450 -450
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-gestures/references/gesture-patterns.md +425 -425
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-layout-components/SKILL.md +336 -336
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-layout-components/references/form.md +97 -97
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-layout-components/references/grids.md +69 -69
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-layout-components/references/list.md +99 -99
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-layout-components/references/scrollview.md +147 -147
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-liquid-glass/SKILL.md +325 -325
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-liquid-glass/references/liquid-glass.md +387 -387
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-navigation/SKILL.md +262 -262
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-navigation/references/deeplinks.md +207 -207
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-navigation/references/navigationstack.md +177 -177
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-navigation/references/sheets.md +169 -169
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-navigation/references/tabview.md +178 -178
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-patterns/SKILL.md +381 -381
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-patterns/references/architecture-patterns.md +486 -486
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-patterns/references/deprecated-migration.md +1097 -1097
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-patterns/references/design-polish.md +780 -780
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-patterns/references/platform-and-sharing.md +696 -696
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-performance/SKILL.md +491 -491
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +46 -46
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +29 -29
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-performance/references/understanding-hangs-in-your-app.md +33 -33
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-performance/references/understanding-improving-swiftui-performance.md +52 -52
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-uikit-interop/SKILL.md +428 -428
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-uikit-interop/references/hosting-migration.md +534 -534
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/swiftui-uikit-interop/references/representable-recipes.md +1133 -1133
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/tipkit/SKILL.md +494 -494
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/tipkit/references/tipkit-patterns.md +782 -782
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/vision-framework/SKILL.md +475 -475
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/vision-framework/references/vision-requests.md +736 -736
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/vision-framework/references/visionkit-scanner.md +738 -738
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/weatherkit/SKILL.md +410 -410
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/weatherkit/references/weatherkit-patterns.md +567 -567
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/widgetkit/SKILL.md +497 -497
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/widgetkit/references/widgetkit-advanced.md +871 -871
- package/src/.agents/skills/ui-ux-pro-custom/data/typography.csv +57 -57
- package/src/.agents/skills/ui-ux-pro-custom/data/ui-reasoning.csv +101 -101
- package/src/.agents/skills/ui-ux-pro-custom/data/ux-guidelines.csv +99 -99
- package/src/.agents/skills/ui-ux-pro-custom/data/web-interface.csv +31 -31
- package/src/.agents/skills/ui-ux-pro-custom/scripts/core.py +253 -253
- package/src/.agents/skills/ui-ux-pro-custom/scripts/design_system.py +1067 -1067
- package/src/.agents/skills/ui-ux-pro-custom/scripts/search.py +114 -114
- package/src/.agents/skills/ux-audit/SKILL.md +150 -150
- package/src/.agents/skills/websocket-engineer/SKILL.md +168 -168
- package/src/.agents/skills/websocket-engineer/references/alternatives.md +391 -391
- package/src/.agents/skills/websocket-engineer/references/patterns.md +400 -400
- package/src/.agents/skills/websocket-engineer/references/protocol.md +195 -195
- package/src/.agents/skills/websocket-engineer/references/scaling.md +333 -333
- package/src/.agents/skills/websocket-engineer/references/security.md +474 -474
- package/src/.agents/skills/writing-skills/SKILL.md +655 -655
- package/src/.agents/skills/writing-skills/anthropic-best-practices.md +1150 -1150
- package/src/.agents/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -189
- package/src/.agents/skills/writing-skills/graphviz-conventions.dot +171 -171
- package/src/.agents/skills/writing-skills/persuasion-principles.md +187 -187
- package/src/.agents/skills/writing-skills/render-graphs.js +168 -168
- package/src/.agents/skills/writing-skills/testing-skills-with-subagents.md +384 -384
- package/src/.claude/commands/bmad-track-compact.md +1 -1
- package/src/.claude/commands/bmad-track-extended.md +1 -1
- package/src/.claude/commands/bmad-track-large.md +1 -1
- package/src/.claude/commands/bmad-track-medium.md +1 -1
- package/src/.claude/commands/bmad-track-nano.md +1 -1
- package/src/.claude/commands/bmad-track-rv.md +1 -1
- package/src/.claude/commands/bmad-track-small.md +1 -1
- package/src/.claude/commands/master-orchestrator.md +15 -0
- package/src/_memory/config.yaml +11 -11
- package/src/_memory/master-orchestrator-sidecar/instructions.md +85 -32
- package/src/_memory/skills/nimbalyst-tracking/SKILL.md +103 -103
- package/src/_memory/skills/writing-skills/SKILL.md +655 -655
- package/src/bmb/agents/agent-builder.md +59 -59
- package/src/bmb/agents/module-builder.md +60 -60
- package/src/bmb/agents/workflow-builder.md +61 -61
- package/src/bmb/config.yaml +12 -12
- package/src/bmb/module-help.csv +13 -13
- package/src/bmb/workflows/agent/data/agent-architecture.md +258 -258
- package/src/bmb/workflows/agent/data/agent-compilation.md +185 -185
- package/src/bmb/workflows/agent/data/agent-menu-patterns.md +189 -189
- package/src/bmb/workflows/agent/data/agent-metadata.md +133 -133
- package/src/bmb/workflows/agent/data/agent-validation.md +111 -111
- package/src/bmb/workflows/agent/data/brainstorm-context.md +96 -96
- package/src/bmb/workflows/agent/data/communication-presets.csv +61 -61
- package/src/bmb/workflows/agent/data/critical-actions.md +75 -75
- package/src/bmb/workflows/agent/data/persona-properties.md +252 -252
- package/src/bmb/workflows/agent/data/principles-crafting.md +142 -142
- package/src/bmb/workflows/agent/data/reference/module-examples/architect.md +68 -68
- package/src/bmb/workflows/agent/data/reference/with-sidecar/journal-keeper/journal-keeper-sidecar/entries/yy-mm-dd-entry-template.md +16 -16
- package/src/bmb/workflows/agent/data/understanding-agent-types.md +126 -126
- package/src/bmb/workflows/agent/steps-c/step-01-brainstorm.md +129 -129
- package/src/bmb/workflows/agent/steps-c/step-02-discovery.md +170 -170
- package/src/bmb/workflows/agent/steps-c/step-03-sidecar-metadata.md +309 -309
- package/src/bmb/workflows/agent/steps-c/step-04-persona.md +213 -213
- package/src/bmb/workflows/agent/steps-c/step-05-commands-menu.md +179 -179
- package/src/bmb/workflows/agent/steps-c/step-06-activation.md +278 -278
- package/src/bmb/workflows/agent/steps-c/step-07-build-agent.md +316 -316
- package/src/bmb/workflows/agent/steps-c/step-08-celebrate.md +247 -247
- package/src/bmb/workflows/agent/steps-e/e-01-load-existing.md +221 -221
- package/src/bmb/workflows/agent/steps-e/e-02-discover-edits.md +195 -195
- package/src/bmb/workflows/agent/steps-e/e-04-sidecar-metadata.md +126 -126
- package/src/bmb/workflows/agent/steps-e/e-05-persona.md +135 -135
- package/src/bmb/workflows/agent/steps-e/e-06-commands-menu.md +123 -123
- package/src/bmb/workflows/agent/steps-e/e-07-activation.md +124 -124
- package/src/bmb/workflows/agent/steps-e/e-08-edit-agent.md +197 -197
- package/src/bmb/workflows/agent/steps-e/e-09-celebrate.md +155 -155
- package/src/bmb/workflows/agent/steps-v/v-01-load-review.md +137 -137
- package/src/bmb/workflows/agent/steps-v/v-02a-validate-metadata.md +116 -116
- package/src/bmb/workflows/agent/steps-v/v-02b-validate-persona.md +124 -124
- package/src/bmb/workflows/agent/steps-v/v-02c-validate-menu.md +127 -127
- package/src/bmb/workflows/agent/steps-v/v-02d-validate-structure.md +134 -134
- package/src/bmb/workflows/agent/steps-v/v-02e-validate-sidecar.md +134 -134
- package/src/bmb/workflows/agent/steps-v/v-03-summary.md +104 -104
- package/src/bmb/workflows/agent/templates/agent-plan.template.md +5 -5
- package/src/bmb/workflows/agent/templates/agent-template.md +89 -89
- package/src/bmb/workflows/agent/workflow-create-agent.md +72 -72
- package/src/bmb/workflows/agent/workflow-edit-agent.md +75 -75
- package/src/bmb/workflows/agent/workflow-validate-agent.md +73 -73
- package/src/bmb/workflows/module/data/agent-architecture.md +179 -179
- package/src/bmb/workflows/module/data/agent-spec-template.md +79 -79
- package/src/bmb/workflows/module/data/module-standards.md +263 -263
- package/src/bmb/workflows/module/data/module-yaml-conventions.md +392 -392
- package/src/bmb/workflows/module/module-help-generate.md +254 -254
- package/src/bmb/workflows/module/steps-b/step-01-welcome.md +148 -148
- package/src/bmb/workflows/module/steps-b/step-02-spark.md +141 -141
- package/src/bmb/workflows/module/steps-b/step-03-module-type.md +149 -149
- package/src/bmb/workflows/module/steps-b/step-04-vision.md +83 -83
- package/src/bmb/workflows/module/steps-b/step-05-identity.md +97 -97
- package/src/bmb/workflows/module/steps-b/step-06-users.md +86 -86
- package/src/bmb/workflows/module/steps-b/step-07-value.md +76 -76
- package/src/bmb/workflows/module/steps-b/step-08-agents.md +97 -97
- package/src/bmb/workflows/module/steps-b/step-09-workflows.md +83 -83
- package/src/bmb/workflows/module/steps-b/step-10-tools.md +91 -91
- package/src/bmb/workflows/module/steps-b/step-11-scenarios.md +84 -84
- package/src/bmb/workflows/module/steps-b/step-12-creative.md +95 -95
- package/src/bmb/workflows/module/steps-b/step-13-review.md +105 -105
- package/src/bmb/workflows/module/steps-b/step-14-finalize.md +117 -117
- package/src/bmb/workflows/module/steps-c/step-01-load-brief.md +179 -179
- package/src/bmb/workflows/module/steps-c/step-01b-continue.md +82 -82
- package/src/bmb/workflows/module/steps-c/step-02-structure.md +105 -105
- package/src/bmb/workflows/module/steps-c/step-03-config.md +119 -119
- package/src/bmb/workflows/module/steps-c/step-04-agents.md +168 -168
- package/src/bmb/workflows/module/steps-c/step-05-workflows.md +184 -184
- package/src/bmb/workflows/module/steps-c/step-06-docs.md +401 -401
- package/src/bmb/workflows/module/steps-c/step-07-complete.md +152 -152
- package/src/bmb/workflows/module/steps-e/step-01-load-target.md +81 -81
- package/src/bmb/workflows/module/steps-e/step-02-select-edit.md +77 -77
- package/src/bmb/workflows/module/steps-e/step-03-apply-edit.md +77 -77
- package/src/bmb/workflows/module/steps-e/step-04-review.md +80 -80
- package/src/bmb/workflows/module/steps-e/step-05-confirm.md +75 -75
- package/src/bmb/workflows/module/steps-v/step-01-load-target.md +96 -96
- package/src/bmb/workflows/module/steps-v/step-02-file-structure.md +93 -93
- package/src/bmb/workflows/module/steps-v/step-03-module-yaml.md +99 -99
- package/src/bmb/workflows/module/steps-v/step-04-agent-specs.md +152 -152
- package/src/bmb/workflows/module/steps-v/step-05-workflow-specs.md +152 -152
- package/src/bmb/workflows/module/steps-v/step-06-documentation.md +143 -143
- package/src/bmb/workflows/module/steps-v/step-07-installation.md +102 -102
- package/src/bmb/workflows/module/steps-v/step-08-report.md +197 -197
- package/src/bmb/workflows/module/templates/brief-template.md +154 -154
- package/src/bmb/workflows/module/templates/workflow-spec-template.md +96 -96
- package/src/bmb/workflows/module/workflow-create-module-brief.md +71 -71
- package/src/bmb/workflows/module/workflow-create-module.md +86 -86
- package/src/bmb/workflows/module/workflow-edit-module.md +66 -66
- package/src/bmb/workflows/module/workflow-validate-module.md +66 -66
- package/src/bmb/workflows/workflow/data/architecture.md +150 -150
- package/src/bmb/workflows/workflow/data/common-workflow-tools.csv +19 -19
- package/src/bmb/workflows/workflow/data/csv-data-file-standards.md +53 -53
- package/src/bmb/workflows/workflow/data/frontmatter-standards.md +184 -184
- package/src/bmb/workflows/workflow/data/input-discovery-standards.md +191 -191
- package/src/bmb/workflows/workflow/data/intent-vs-prescriptive-spectrum.md +44 -44
- package/src/bmb/workflows/workflow/data/menu-handling-standards.md +133 -133
- package/src/bmb/workflows/workflow/data/output-format-standards.md +135 -135
- package/src/bmb/workflows/workflow/data/step-file-rules.md +235 -235
- package/src/bmb/workflows/workflow/data/step-type-patterns.md +257 -257
- package/src/bmb/workflows/workflow/data/subprocess-optimization-patterns.md +188 -188
- package/src/bmb/workflows/workflow/data/trimodal-workflow-structure.md +164 -164
- package/src/bmb/workflows/workflow/data/workflow-chaining-standards.md +222 -222
- package/src/bmb/workflows/workflow/data/workflow-examples.md +232 -232
- package/src/bmb/workflows/workflow/data/workflow-type-criteria.md +134 -134
- package/src/bmb/workflows/workflow/steps-c/step-00-conversion.md +263 -263
- package/src/bmb/workflows/workflow/steps-c/step-01-discovery.md +194 -194
- package/src/bmb/workflows/workflow/steps-c/step-01b-continuation.md +3 -3
- package/src/bmb/workflows/workflow/steps-c/step-02-classification.md +270 -270
- package/src/bmb/workflows/workflow/steps-c/step-03-requirements.md +283 -283
- package/src/bmb/workflows/workflow/steps-c/step-04-tools.md +282 -282
- package/src/bmb/workflows/workflow/steps-c/step-05-plan-review.md +243 -243
- package/src/bmb/workflows/workflow/steps-c/step-06-design.md +330 -330
- package/src/bmb/workflows/workflow/steps-c/step-07-foundation.md +239 -239
- package/src/bmb/workflows/workflow/steps-c/step-08-build-step-01.md +379 -379
- package/src/bmb/workflows/workflow/steps-c/step-09-build-next-step.md +350 -350
- package/src/bmb/workflows/workflow/steps-c/step-10-confirmation.md +322 -322
- package/src/bmb/workflows/workflow/steps-c/step-11-completion.md +191 -191
- package/src/bmb/workflows/workflow/steps-e/step-e-01-assess-workflow.md +237 -237
- package/src/bmb/workflows/workflow/steps-e/step-e-02-discover-edits.md +251 -251
- package/src/bmb/workflows/workflow/steps-e/step-e-03-fix-validation.md +254 -254
- package/src/bmb/workflows/workflow/steps-e/step-e-04-direct-edit.md +277 -277
- package/src/bmb/workflows/workflow/steps-e/step-e-05-apply-edit.md +154 -154
- package/src/bmb/workflows/workflow/steps-e/step-e-06-validate-after.md +190 -190
- package/src/bmb/workflows/workflow/steps-e/step-e-07-complete.md +206 -206
- package/src/bmb/workflows/workflow/steps-v/step-01-validate-max-mode.md +109 -109
- package/src/bmb/workflows/workflow/steps-v/step-01-validate.md +221 -221
- package/src/bmb/workflows/workflow/steps-v/step-01b-structure.md +152 -152
- package/src/bmb/workflows/workflow/steps-v/step-02-frontmatter-validation.md +199 -199
- package/src/bmb/workflows/workflow/steps-v/step-02b-path-violations.md +265 -265
- package/src/bmb/workflows/workflow/steps-v/step-03-menu-validation.md +164 -164
- package/src/bmb/workflows/workflow/steps-v/step-04-step-type-validation.md +211 -211
- package/src/bmb/workflows/workflow/steps-v/step-05-output-format-validation.md +200 -200
- package/src/bmb/workflows/workflow/steps-v/step-06-validation-design-check.md +195 -195
- package/src/bmb/workflows/workflow/steps-v/step-07-instruction-style-check.md +209 -209
- package/src/bmb/workflows/workflow/steps-v/step-08-collaborative-experience-check.md +199 -199
- package/src/bmb/workflows/workflow/steps-v/step-08b-subprocess-optimization.md +179 -179
- package/src/bmb/workflows/workflow/steps-v/step-09-cohesive-review.md +186 -186
- package/src/bmb/workflows/workflow/steps-v/step-10-report-complete.md +154 -154
- package/src/bmb/workflows/workflow/steps-v/step-11-plan-validation.md +237 -237
- package/src/bmb/workflows/workflow/templates/minimal-output-template.md +11 -11
- package/src/bmb/workflows/workflow/templates/step-01-init-continuable-template.md +241 -241
- package/src/bmb/workflows/workflow/templates/step-1b-template.md +224 -224
- package/src/bmb/workflows/workflow/templates/step-template.md +294 -294
- package/src/bmb/workflows/workflow/templates/workflow-template.md +102 -102
- package/src/bmb/workflows/workflow/workflow-create-workflow.md +79 -79
- package/src/bmb/workflows/workflow/workflow-edit-workflow.md +65 -65
- package/src/bmb/workflows/workflow/workflow-rework-workflow.md +65 -65
- package/src/bmb/workflows/workflow/workflow-validate-max-parallel-workflow.md +66 -66
- package/src/bmb/workflows/workflow/workflow-validate-workflow.md +65 -65
- package/src/bmm/agents/analyst.md +104 -104
- package/src/bmm/agents/dev.md +100 -100
- package/src/bmm/agents/qa.md +100 -90
- package/src/bmm/agents/review-agent.md +1 -1
- package/src/bmm/agents/tech-writer/tech-writer.md +94 -94
- package/src/bmm/module-help.csv +31 -31
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-01-init.md +115 -115
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-01b-continue.md +107 -107
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-02-vision.md +141 -141
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-03-users.md +144 -144
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-04-metrics.md +147 -147
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-05-scope.md +161 -161
- package/src/bmm/workflows/1-analysis/create-product-brief/steps/step-06-complete.md +99 -99
- package/src/bmm/workflows/1-analysis/create-product-brief/workflow.md +57 -57
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-01-init.md +87 -87
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-02-domain-analysis.md +156 -156
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-03-competitive-landscape.md +165 -165
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-04-regulatory-focus.md +140 -140
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-05-technical-trends.md +152 -152
- package/src/bmm/workflows/1-analysis/research/domain-steps/step-06-research-synthesis.md +345 -345
- package/src/bmm/workflows/1-analysis/research/market-steps/step-01-init.md +92 -92
- package/src/bmm/workflows/1-analysis/research/market-steps/step-02-customer-behavior.md +164 -164
- package/src/bmm/workflows/1-analysis/research/market-steps/step-03-customer-pain-points.md +174 -174
- package/src/bmm/workflows/1-analysis/research/market-steps/step-04-customer-decisions.md +184 -184
- package/src/bmm/workflows/1-analysis/research/market-steps/step-05-competitive-analysis.md +105 -105
- package/src/bmm/workflows/1-analysis/research/market-steps/step-06-research-completion.md +360 -360
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-01-init.md +87 -87
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-02-technical-overview.md +165 -165
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-03-integration-patterns.md +174 -174
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-04-architectural-patterns.md +141 -141
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-05-implementation-research.md +159 -159
- package/src/bmm/workflows/1-analysis/research/technical-steps/step-06-research-synthesis.md +387 -387
- package/src/bmm/workflows/1-analysis/research/workflow-domain-research.md +54 -54
- package/src/bmm/workflows/1-analysis/research/workflow-market-research.md +54 -54
- package/src/bmm/workflows/1-analysis/research/workflow-technical-research.md +54 -54
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-01b-continue.md +100 -100
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-02-discovery.md +160 -160
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-02b-vision.md +88 -88
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-02c-executive-summary.md +99 -99
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-03-success.md +169 -169
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-04-journeys.md +156 -156
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-05-domain.md +136 -136
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-06-innovation.md +176 -176
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-07-project-type.md +184 -184
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-08-scoping.md +174 -174
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-09-functional.md +175 -175
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-10-nonfunctional.md +189 -189
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-11-polish.md +162 -162
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-12-complete.md +79 -79
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-01-discovery.md +183 -183
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-01b-legacy-conversion.md +149 -149
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-02-review.md +187 -187
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-03-edit.md +192 -192
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-e/step-e-04-complete.md +108 -108
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-01-discovery.md +166 -166
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-02-format-detection.md +131 -131
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-02b-parity-check.md +150 -150
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-03-density-validation.md +118 -118
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-04-brief-coverage-validation.md +155 -155
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-05-measurability-validation.md +170 -170
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-06-traceability-validation.md +158 -158
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-07-implementation-leakage-validation.md +147 -147
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-08-domain-compliance-validation.md +182 -182
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-09-project-type-validation.md +202 -202
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-10-smart-validation.md +148 -148
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-11-holistic-quality-validation.md +201 -201
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-12-completeness-validation.md +179 -179
- package/src/bmm/workflows/2-plan-workflows/create-prd/steps-v/step-v-13-report-complete.md +164 -164
- package/src/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md +65 -65
- package/src/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md +65 -65
- package/src/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md +63 -63
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-01b-continue.md +63 -63
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-02-discovery.md +106 -106
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-03-core-experience.md +111 -111
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-04-emotional-response.md +115 -115
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-05-inspiration.md +127 -127
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-06-design-system.md +167 -167
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-07-defining-experience.md +143 -143
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-08-visual-foundation.md +118 -118
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-09-design-directions.md +154 -154
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-10-user-journeys.md +136 -136
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-11-component-strategy.md +165 -165
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-12-ux-patterns.md +135 -135
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-13-responsive-accessibility.md +192 -192
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-14-complete.md +101 -101
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md +45 -45
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-01-document-discovery.md +185 -185
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-02-prd-analysis.md +129 -129
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-03-epic-coverage-validation.md +130 -130
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-04-ux-alignment.md +93 -93
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-05-epic-quality-review.md +196 -196
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-06-final-assessment.md +129 -129
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md +54 -54
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-01b-continue.md +82 -82
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-02-context.md +106 -106
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-03-starter.md +138 -138
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-04-decisions.md +129 -129
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-05-patterns.md +166 -166
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-06-structure.md +186 -186
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-07-validation.md +163 -163
- package/src/bmm/workflows/3-solutioning/create-architecture/steps/step-08-complete.md +38 -38
- package/src/bmm/workflows/3-solutioning/create-architecture/workflow.md +49 -49
- package/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-02-design-epics.md +124 -124
- package/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-03-create-stories.md +122 -122
- package/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-04-final-validation.md +84 -84
- package/src/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md +58 -58
- package/src/bmm/workflows/4-implementation/code-review/workflow.yaml +43 -43
- package/src/bmm/workflows/4-implementation/correct-course/workflow.yaml +53 -53
- package/src/bmm/workflows/4-implementation/create-story/checklist.md +159 -159
- package/src/bmm/workflows/4-implementation/create-story/template.md +79 -79
- package/src/bmm/workflows/4-implementation/create-story/workflow.yaml +52 -52
- package/src/bmm/workflows/4-implementation/dev-story/workflow.yaml +20 -20
- package/src/bmm/workflows/4-implementation/retrospective/workflow.yaml +52 -52
- package/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml +52 -52
- package/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml +25 -25
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-01-mode-detection.md +158 -158
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-02-context-gathering.md +122 -122
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-03-execute.md +93 -93
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-04-self-check.md +93 -93
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-05-adversarial-review.md +87 -87
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-06-resolve-findings.md +146 -146
- package/src/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md +50 -50
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-02-investigate.md +152 -152
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-03-generate.md +123 -123
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-04-review.md +201 -201
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md +79 -79
- package/src/bmm/workflows/document-project/workflow.yaml +22 -22
- package/src/bmm/workflows/generate-project-context/steps/step-01-discover.md +184 -184
- package/src/bmm/workflows/generate-project-context/steps/step-02-generate.md +322 -322
- package/src/bmm/workflows/generate-project-context/steps/step-03-complete.md +235 -235
- package/src/bmm/workflows/generate-project-context/workflow.md +49 -49
- package/src/bmm/workflows/qa/automate/workflow.yaml +233 -233
- package/src/bmm/workflows/qa-generate-e2e-tests/workflow.yaml +42 -42
- package/src/core/agents/master-orchestrator.md +3 -3
- package/src/core/config.yaml +9 -9
- package/src/core/module-help.csv +10 -10
- package/src/core/scripts/generate-loop-report.py +72 -72
- package/src/core/tasks/editorial-review-prose.xml +101 -101
- package/src/core/tasks/editorial-review-structure.xml +207 -207
- package/src/core/tasks/help.md +86 -86
- package/src/core/tasks/index-docs.xml +64 -64
- package/src/core/tasks/review-adversarial-general.xml +66 -66
- package/src/core/tasks/review-adversarial-loop.xml +46 -46
- package/src/core/tasks/review-edge-case-hunter.xml +63 -63
- package/src/core/tasks/review-party-loop.xml +46 -46
- package/src/core/tasks/shard-doc.xml +107 -107
- package/src/core/tasks/workflow.xml +235 -235
- package/src/core/templates/review-loop-report.html +88 -88
- package/src/core/templates/review-loop-report.md +5 -5
- package/src/core/workflows/advanced-elicitation/workflow.xml +117 -117
- package/src/core/workflows/brainstorming/steps/step-01-session-setup.md +212 -212
- package/src/core/workflows/brainstorming/steps/step-01b-continue.md +122 -122
- package/src/core/workflows/brainstorming/steps/step-02a-user-selected.md +225 -225
- package/src/core/workflows/brainstorming/steps/step-02b-ai-recommended.md +237 -237
- package/src/core/workflows/brainstorming/steps/step-02c-random-selection.md +209 -209
- package/src/core/workflows/brainstorming/steps/step-02d-progressive-flow.md +264 -264
- package/src/core/workflows/brainstorming/steps/step-02e-deep-dive.md +68 -68
- package/src/core/workflows/brainstorming/steps/step-03-technique-execution.md +403 -403
- package/src/core/workflows/brainstorming/steps/step-04-idea-organization.md +303 -303
- package/src/core/workflows/brainstorming/workflow.md +60 -60
- package/src/core/workflows/extract-trackers/workflow.md +45 -45
- package/src/core/workflows/party-mode/steps/step-01-agent-loading.md +142 -142
- package/src/core/workflows/party-mode/workflow.md +194 -194
- package/src/docs/dev/tmux/actions_popup.py +291 -291
- package/src/docs/dev/tmux/tmux-setup.md +62 -1
|
@@ -1,869 +1,869 @@
|
|
|
1
|
-
# Image Loading and Caching Patterns
|
|
2
|
-
|
|
3
|
-
Complete patterns for efficient image handling in iOS apps, from basic AsyncImage usage through production-ready caching and loading pipelines. All patterns use modern Swift async/await and target iOS 26 with Swift 6.2, backward-compatible to iOS 16 unless noted.
|
|
4
|
-
|
|
5
|
-
## Contents
|
|
6
|
-
- AsyncImage Patterns
|
|
7
|
-
- NSCache-Based In-Memory Cache
|
|
8
|
-
- URLCache-Based Disk Caching
|
|
9
|
-
- Image Downsampling with CGImageSource
|
|
10
|
-
- Image Prefetching for Lists and Grids
|
|
11
|
-
- HEIF/HEIC Handling
|
|
12
|
-
- Compression Before Upload
|
|
13
|
-
- Memory Budget Management
|
|
14
|
-
- Complete Image Loading Pipeline
|
|
15
|
-
|
|
16
|
-
## AsyncImage Patterns
|
|
17
|
-
|
|
18
|
-
AsyncImage (iOS 15+) provides built-in async image loading from a URL. Suitable for simple cases but has significant limitations for production use.
|
|
19
|
-
|
|
20
|
-
### Basic Usage
|
|
21
|
-
|
|
22
|
-
```swift
|
|
23
|
-
AsyncImage(url: URL(string: "https://example.com/photo.jpg"))
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
### Phase Handling
|
|
27
|
-
|
|
28
|
-
```swift
|
|
29
|
-
AsyncImage(url: imageURL) { phase in
|
|
30
|
-
switch phase {
|
|
31
|
-
case .empty:
|
|
32
|
-
ProgressView()
|
|
33
|
-
.frame(width: 200, height: 200)
|
|
34
|
-
case .success(let image):
|
|
35
|
-
image
|
|
36
|
-
.resizable()
|
|
37
|
-
.aspectRatio(contentMode: .fill)
|
|
38
|
-
.frame(width: 200, height: 200)
|
|
39
|
-
.clipped()
|
|
40
|
-
case .failure:
|
|
41
|
-
Image(systemName: "photo")
|
|
42
|
-
.font(.largeTitle)
|
|
43
|
-
.foregroundStyle(.secondary)
|
|
44
|
-
.frame(width: 200, height: 200)
|
|
45
|
-
@unknown default:
|
|
46
|
-
EmptyView()
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Custom Transition
|
|
52
|
-
|
|
53
|
-
```swift
|
|
54
|
-
AsyncImage(url: imageURL, transaction: Transaction(animation: .easeIn(duration: 0.3))) { phase in
|
|
55
|
-
switch phase {
|
|
56
|
-
case .success(let image):
|
|
57
|
-
image
|
|
58
|
-
.resizable()
|
|
59
|
-
.aspectRatio(contentMode: .fill)
|
|
60
|
-
.transition(.opacity)
|
|
61
|
-
default:
|
|
62
|
-
Color.secondary.opacity(0.2)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
.frame(width: 200, height: 200)
|
|
66
|
-
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### Limitations
|
|
70
|
-
|
|
71
|
-
AsyncImage has several shortcomings for production apps:
|
|
72
|
-
|
|
73
|
-
- **No caching across redraws**: images re-download when the view is recreated.
|
|
74
|
-
- **No prefetching**: cannot load images ahead of scroll position.
|
|
75
|
-
- **No custom URLSession**: uses the shared session with no cache policy control.
|
|
76
|
-
- **No access to raw data**: cannot process, downsample, or persist the image data.
|
|
77
|
-
- **No cancellation control**: tied entirely to the view lifecycle.
|
|
78
|
-
|
|
79
|
-
For anything beyond simple, low-volume image display, use a custom loading pipeline.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## NSCache-Based In-Memory Cache
|
|
84
|
-
|
|
85
|
-
An actor-isolated in-memory image cache backed by NSCache. Thread-safe and automatically evicts entries under memory pressure.
|
|
86
|
-
|
|
87
|
-
```swift
|
|
88
|
-
import UIKit
|
|
89
|
-
|
|
90
|
-
actor ImageCache {
|
|
91
|
-
static let shared = ImageCache()
|
|
92
|
-
|
|
93
|
-
private let cache = NSCache<NSString, UIImage>()
|
|
94
|
-
private var inFlightTasks: [URL: Task<UIImage?, Never>] = [:]
|
|
95
|
-
|
|
96
|
-
init(countLimit: Int = 100, totalCostLimit: Int = 50 * 1024 * 1024) {
|
|
97
|
-
cache.countLimit = countLimit
|
|
98
|
-
cache.totalCostLimit = totalCostLimit // 50 MB default
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
func image(for url: URL) -> UIImage? {
|
|
102
|
-
cache.object(forKey: url.absoluteString as NSString)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
func store(_ image: UIImage, for url: URL) {
|
|
106
|
-
let cost = image.jpegData(compressionQuality: 1.0)?.count ?? 0
|
|
107
|
-
cache.setObject(image, forKey: url.absoluteString as NSString, cost: cost)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
func removeImage(for url: URL) {
|
|
111
|
-
cache.removeObject(forKey: url.absoluteString as NSString)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
func removeAll() {
|
|
115
|
-
cache.removeAllObjects()
|
|
116
|
-
inFlightTasks.removeAll()
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/// Fetch an image with request coalescing. Multiple callers for the same URL
|
|
120
|
-
/// share a single network request.
|
|
121
|
-
func fetch(from url: URL, session: URLSession = .shared) async -> UIImage? {
|
|
122
|
-
// Return cached image immediately
|
|
123
|
-
if let cached = image(for: url) {
|
|
124
|
-
return cached
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Coalesce duplicate in-flight requests
|
|
128
|
-
if let existingTask = inFlightTasks[url] {
|
|
129
|
-
return await existingTask.value
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
let task = Task<UIImage?, Never> {
|
|
133
|
-
defer { inFlightTasks[url] = nil }
|
|
134
|
-
|
|
135
|
-
guard let (data, response) = try? await session.data(from: url),
|
|
136
|
-
let httpResponse = response as? HTTPURLResponse,
|
|
137
|
-
httpResponse.statusCode == 200,
|
|
138
|
-
let image = UIImage(data: data) else {
|
|
139
|
-
return nil
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
store(image, for: url)
|
|
143
|
-
return image
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
inFlightTasks[url] = task
|
|
147
|
-
return await task.value
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### Usage in SwiftUI
|
|
153
|
-
|
|
154
|
-
```swift
|
|
155
|
-
struct CachedImageView: View {
|
|
156
|
-
let url: URL
|
|
157
|
-
@State private var image: UIImage?
|
|
158
|
-
|
|
159
|
-
var body: some View {
|
|
160
|
-
Group {
|
|
161
|
-
if let image {
|
|
162
|
-
Image(uiImage: image)
|
|
163
|
-
.resizable()
|
|
164
|
-
.aspectRatio(contentMode: .fill)
|
|
165
|
-
} else {
|
|
166
|
-
Color.secondary.opacity(0.2)
|
|
167
|
-
.overlay(ProgressView())
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
.task(id: url) {
|
|
171
|
-
image = await ImageCache.shared.fetch(from: url)
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
---
|
|
178
|
-
|
|
179
|
-
## URLCache-Based Disk Caching
|
|
180
|
-
|
|
181
|
-
Configure URLCache for persistent image caching that survives app restarts. URLCache handles HTTP cache headers automatically.
|
|
182
|
-
|
|
183
|
-
### Configuring a Dedicated URLSession
|
|
184
|
-
|
|
185
|
-
```swift
|
|
186
|
-
enum ImageSessionConfiguration {
|
|
187
|
-
static func makeSession() -> URLSession {
|
|
188
|
-
let config = URLSessionConfiguration.default
|
|
189
|
-
|
|
190
|
-
// 50 MB memory / 200 MB disk
|
|
191
|
-
config.urlCache = URLCache(
|
|
192
|
-
memoryCapacity: 50 * 1024 * 1024,
|
|
193
|
-
diskCapacity: 200 * 1024 * 1024,
|
|
194
|
-
directory: cacheDirectory
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
config.requestCachePolicy = .returnCacheDataElseLoad
|
|
198
|
-
config.httpMaximumConnectionsPerHost = 6
|
|
199
|
-
|
|
200
|
-
return URLSession(configuration: config)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
private static var cacheDirectory: URL {
|
|
204
|
-
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
|
205
|
-
.appendingPathComponent("ImageCache", isDirectory: true)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### Cache-Aware Request
|
|
211
|
-
|
|
212
|
-
```swift
|
|
213
|
-
func cachedImageData(from url: URL, session: URLSession) async throws -> Data {
|
|
214
|
-
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
|
|
215
|
-
let (data, _) = try await session.data(for: request)
|
|
216
|
-
return data
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### Force Refresh (Bypass Cache)
|
|
221
|
-
|
|
222
|
-
```swift
|
|
223
|
-
func refreshImageData(from url: URL, session: URLSession) async throws -> Data {
|
|
224
|
-
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
|
|
225
|
-
let (data, _) = try await session.data(for: request)
|
|
226
|
-
return data
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### Cache Cleanup
|
|
231
|
-
|
|
232
|
-
```swift
|
|
233
|
-
func cleanImageCache(session: URLSession) {
|
|
234
|
-
session.configuration.urlCache?.removeAllCachedResponses()
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
func removeCachedImage(for url: URL, session: URLSession) {
|
|
238
|
-
let request = URLRequest(url: url)
|
|
239
|
-
session.configuration.urlCache?.removeCachedResponse(for: request)
|
|
240
|
-
}
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
---
|
|
244
|
-
|
|
245
|
-
## Image Downsampling with CGImageSource
|
|
246
|
-
|
|
247
|
-
Loading a full-resolution image into memory then scaling it in the view wastes significant memory. Downsampling at decode time creates a smaller bitmap directly.
|
|
248
|
-
|
|
249
|
-
### Downsample Function
|
|
250
|
-
|
|
251
|
-
```swift
|
|
252
|
-
import ImageIO
|
|
253
|
-
import UIKit
|
|
254
|
-
|
|
255
|
-
func downsample(data: Data, to pointSize: CGSize, scale: CGFloat = UITraitCollection.current.displayScale) -> UIImage? {
|
|
256
|
-
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
|
|
257
|
-
|
|
258
|
-
let options: [CFString: Any] = [
|
|
259
|
-
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
260
|
-
kCGImageSourceShouldCacheImmediately: true,
|
|
261
|
-
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
262
|
-
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
|
|
263
|
-
]
|
|
264
|
-
|
|
265
|
-
guard let source = CGImageSourceCreateWithData(data as CFData, nil),
|
|
266
|
-
let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
|
|
267
|
-
return nil
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return UIImage(cgImage: cgImage)
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
### Downsample from URL
|
|
275
|
-
|
|
276
|
-
```swift
|
|
277
|
-
func downsample(url: URL, to pointSize: CGSize, scale: CGFloat = UITraitCollection.current.displayScale) -> UIImage? {
|
|
278
|
-
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
|
|
279
|
-
|
|
280
|
-
let options: [CFString: Any] = [
|
|
281
|
-
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
282
|
-
kCGImageSourceShouldCacheImmediately: true,
|
|
283
|
-
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
284
|
-
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
|
|
285
|
-
]
|
|
286
|
-
|
|
287
|
-
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
|
|
288
|
-
let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
|
|
289
|
-
return nil
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return UIImage(cgImage: cgImage)
|
|
293
|
-
}
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### When to Downsample
|
|
297
|
-
|
|
298
|
-
- Displaying thumbnails in a list or grid (a 4032x3024 photo for a 100x100 cell wastes ~46 MB per image).
|
|
299
|
-
- User-selected photos from the photo library.
|
|
300
|
-
- Images fetched from a server that are larger than the display size.
|
|
301
|
-
- Any time the source image dimensions exceed 2x the display point size.
|
|
302
|
-
|
|
303
|
-
### Memory Savings
|
|
304
|
-
|
|
305
|
-
| Source Size | Display Size | Without Downsampling | With Downsampling |
|
|
306
|
-
|---|---|---|---|
|
|
307
|
-
| 4032x3024 | 100x100 pt @3x | ~46 MB | ~0.35 MB |
|
|
308
|
-
| 4032x3024 | 300x300 pt @3x | ~46 MB | ~3.1 MB |
|
|
309
|
-
| 1920x1080 | 100x100 pt @3x | ~7.9 MB | ~0.35 MB |
|
|
310
|
-
|
|
311
|
-
---
|
|
312
|
-
|
|
313
|
-
## Image Prefetching for Lists and Grids
|
|
314
|
-
|
|
315
|
-
Prefetch images before they scroll into view. Works with both UICollectionView data source prefetching and SwiftUI List.
|
|
316
|
-
|
|
317
|
-
### Prefetch Coordinator
|
|
318
|
-
|
|
319
|
-
```swift
|
|
320
|
-
@Observable
|
|
321
|
-
@MainActor
|
|
322
|
-
final class ImagePrefetcher {
|
|
323
|
-
private let cache: ImageCache
|
|
324
|
-
private let session: URLSession
|
|
325
|
-
private var prefetchTasks: [URL: Task<Void, Never>] = [:]
|
|
326
|
-
|
|
327
|
-
init(cache: ImageCache = .shared,
|
|
328
|
-
session: URLSession = ImageSessionConfiguration.makeSession()) {
|
|
329
|
-
self.cache = cache
|
|
330
|
-
self.session = session
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
func prefetch(urls: [URL]) {
|
|
334
|
-
for url in urls {
|
|
335
|
-
guard prefetchTasks[url] == nil else { continue }
|
|
336
|
-
|
|
337
|
-
prefetchTasks[url] = Task {
|
|
338
|
-
_ = await cache.fetch(from: url, session: session)
|
|
339
|
-
prefetchTasks[url] = nil
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
func cancelPrefetch(urls: [URL]) {
|
|
345
|
-
for url in urls {
|
|
346
|
-
prefetchTasks[url]?.cancel()
|
|
347
|
-
prefetchTasks[url] = nil
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
func cancelAll() {
|
|
352
|
-
prefetchTasks.values.forEach { $0.cancel() }
|
|
353
|
-
prefetchTasks.removeAll()
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
### SwiftUI Integration with ScrollView and LazyVGrid
|
|
359
|
-
|
|
360
|
-
```swift
|
|
361
|
-
struct PhotoGrid: View {
|
|
362
|
-
let photos: [Photo]
|
|
363
|
-
@State private var prefetcher = ImagePrefetcher()
|
|
364
|
-
|
|
365
|
-
private let columns = [GridItem(.adaptive(minimum: 100), spacing: 2)]
|
|
366
|
-
|
|
367
|
-
var body: some View {
|
|
368
|
-
ScrollView {
|
|
369
|
-
LazyVGrid(columns: columns, spacing: 2) {
|
|
370
|
-
ForEach(photos) { photo in
|
|
371
|
-
CachedImageView(url: photo.thumbnailURL)
|
|
372
|
-
.aspectRatio(1, contentMode: .fill)
|
|
373
|
-
.clipped()
|
|
374
|
-
.onAppear {
|
|
375
|
-
prefetchNearby(photo)
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
.onDisappear {
|
|
381
|
-
prefetcher.cancelAll()
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
private func prefetchNearby(_ photo: Photo) {
|
|
386
|
-
guard let index = photos.firstIndex(where: { $0.id == photo.id }) else { return }
|
|
387
|
-
let prefetchRange = (index + 1)..<min(index + 10, photos.count)
|
|
388
|
-
let urls = prefetchRange.map { photos[$0].thumbnailURL }
|
|
389
|
-
prefetcher.prefetch(urls: urls)
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
### UICollectionView Prefetching (UIKit Interop)
|
|
395
|
-
|
|
396
|
-
```swift
|
|
397
|
-
final class PhotoCollectionPrefetcher: NSObject, UICollectionViewDataSourcePrefetching {
|
|
398
|
-
private let prefetcher = ImagePrefetcher()
|
|
399
|
-
private let photos: [Photo]
|
|
400
|
-
|
|
401
|
-
init(photos: [Photo]) {
|
|
402
|
-
self.photos = photos
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
func collectionView(_ collectionView: UICollectionView,
|
|
406
|
-
prefetchItemsAt indexPaths: [IndexPath]) {
|
|
407
|
-
let urls = indexPaths.map { photos[$0.item].thumbnailURL }
|
|
408
|
-
prefetcher.prefetch(urls: urls)
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
func collectionView(_ collectionView: UICollectionView,
|
|
412
|
-
cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
|
|
413
|
-
let urls = indexPaths.map { photos[$0.item].thumbnailURL }
|
|
414
|
-
prefetcher.cancelPrefetch(urls: urls)
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
---
|
|
420
|
-
|
|
421
|
-
## HEIF/HEIC Handling
|
|
422
|
-
|
|
423
|
-
HEIF (High Efficiency Image Format) is the default camera format on modern iPhones. Handle detection, display, and conversion.
|
|
424
|
-
|
|
425
|
-
### Detection
|
|
426
|
-
|
|
427
|
-
```swift
|
|
428
|
-
import UniformTypeIdentifiers
|
|
429
|
-
|
|
430
|
-
func isHEIF(data: Data) -> Bool {
|
|
431
|
-
guard data.count >= 12 else { return false }
|
|
432
|
-
// Check for 'ftyp' box at byte 4
|
|
433
|
-
let ftypRange = data[4..<8]
|
|
434
|
-
return ftypRange.elementsEqual("ftyp".utf8)
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
func isHEIF(url: URL) -> Bool {
|
|
438
|
-
guard let type = UTType(filenameExtension: url.pathExtension) else { return false }
|
|
439
|
-
return type.conforms(to: .heif) || type.conforms(to: .heic)
|
|
440
|
-
}
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
### Conversion to JPEG
|
|
444
|
-
|
|
445
|
-
```swift
|
|
446
|
-
func convertHEICToJPEG(data: Data, compressionQuality: CGFloat = 0.9) -> Data? {
|
|
447
|
-
guard let image = UIImage(data: data) else { return nil }
|
|
448
|
-
return image.jpegData(compressionQuality: compressionQuality)
|
|
449
|
-
}
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
### Conversion to PNG
|
|
453
|
-
|
|
454
|
-
```swift
|
|
455
|
-
func convertHEICToPNG(data: Data) -> Data? {
|
|
456
|
-
guard let image = UIImage(data: data) else { return nil }
|
|
457
|
-
return image.pngData()
|
|
458
|
-
}
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
### Conversion with CGImageDestination (More Control)
|
|
462
|
-
|
|
463
|
-
```swift
|
|
464
|
-
import ImageIO
|
|
465
|
-
|
|
466
|
-
func convertHEICToJPEG(sourceData: Data,
|
|
467
|
-
quality: CGFloat = 0.9,
|
|
468
|
-
preserveMetadata: Bool = true) -> Data? {
|
|
469
|
-
guard let source = CGImageSourceCreateWithData(sourceData as CFData, nil),
|
|
470
|
-
let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else {
|
|
471
|
-
return nil
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
let mutableData = NSMutableData()
|
|
475
|
-
guard let destination = CGImageDestinationCreateWithData(
|
|
476
|
-
mutableData, UTType.jpeg.identifier as CFString, 1, nil
|
|
477
|
-
) else {
|
|
478
|
-
return nil
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
var options: [CFString: Any] = [
|
|
482
|
-
kCGImageDestinationLossyCompressionQuality: quality
|
|
483
|
-
]
|
|
484
|
-
|
|
485
|
-
// Preserve EXIF, GPS, and other metadata
|
|
486
|
-
if preserveMetadata,
|
|
487
|
-
let metadata = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) {
|
|
488
|
-
options[kCGImageDestinationMergeMetadata] = true
|
|
489
|
-
CGImageDestinationAddImage(destination, cgImage, metadata)
|
|
490
|
-
} else {
|
|
491
|
-
CGImageDestinationAddImage(destination, cgImage, options as CFDictionary)
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
guard CGImageDestinationFinalize(destination) else { return nil }
|
|
495
|
-
return mutableData as Data
|
|
496
|
-
}
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
---
|
|
500
|
-
|
|
501
|
-
## Compression Before Upload
|
|
502
|
-
|
|
503
|
-
Reduce file size before uploading to a server. Balance quality and size based on the use case.
|
|
504
|
-
|
|
505
|
-
### JPEG Compression with Target Size
|
|
506
|
-
|
|
507
|
-
```swift
|
|
508
|
-
func compressForUpload(image: UIImage,
|
|
509
|
-
maxBytes: Int = 1_000_000,
|
|
510
|
-
initialQuality: CGFloat = 0.9) -> Data? {
|
|
511
|
-
var quality = initialQuality
|
|
512
|
-
|
|
513
|
-
while quality > 0.1 {
|
|
514
|
-
guard let data = image.jpegData(compressionQuality: quality) else { return nil }
|
|
515
|
-
if data.count <= maxBytes {
|
|
516
|
-
return data
|
|
517
|
-
}
|
|
518
|
-
quality -= 0.1
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Final attempt at minimum quality
|
|
522
|
-
return image.jpegData(compressionQuality: 0.1)
|
|
523
|
-
}
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
### Resize and Compress
|
|
527
|
-
|
|
528
|
-
```swift
|
|
529
|
-
func resizeAndCompress(image: UIImage,
|
|
530
|
-
maxDimension: CGFloat = 1920,
|
|
531
|
-
compressionQuality: CGFloat = 0.8) -> Data? {
|
|
532
|
-
let size = image.size
|
|
533
|
-
let scale: CGFloat
|
|
534
|
-
|
|
535
|
-
if max(size.width, size.height) > maxDimension {
|
|
536
|
-
scale = maxDimension / max(size.width, size.height)
|
|
537
|
-
} else {
|
|
538
|
-
scale = 1.0
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
let newSize = CGSize(width: size.width * scale, height: size.height * scale)
|
|
542
|
-
|
|
543
|
-
let renderer = UIGraphicsImageRenderer(size: newSize)
|
|
544
|
-
let resized = renderer.image { _ in
|
|
545
|
-
image.draw(in: CGRect(origin: .zero, size: newSize))
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
return resized.jpegData(compressionQuality: compressionQuality)
|
|
549
|
-
}
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
### HEIF Compression (Smaller Files)
|
|
553
|
-
|
|
554
|
-
```swift
|
|
555
|
-
func compressAsHEIF(image: UIImage, quality: CGFloat = 0.8) -> Data? {
|
|
556
|
-
guard let cgImage = image.cgImage else { return nil }
|
|
557
|
-
|
|
558
|
-
let mutableData = NSMutableData()
|
|
559
|
-
guard let destination = CGImageDestinationCreateWithData(
|
|
560
|
-
mutableData, UTType.heic.identifier as CFString, 1, nil
|
|
561
|
-
) else {
|
|
562
|
-
return nil
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
let options: [CFString: Any] = [
|
|
566
|
-
kCGImageDestinationLossyCompressionQuality: quality
|
|
567
|
-
]
|
|
568
|
-
|
|
569
|
-
CGImageDestinationAddImage(destination, cgImage, options as CFDictionary)
|
|
570
|
-
guard CGImageDestinationFinalize(destination) else { return nil }
|
|
571
|
-
|
|
572
|
-
return mutableData as Data
|
|
573
|
-
}
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
---
|
|
577
|
-
|
|
578
|
-
## Memory Budget Management
|
|
579
|
-
|
|
580
|
-
Monitor and respond to memory pressure to keep your image pipeline stable.
|
|
581
|
-
|
|
582
|
-
### Memory Warning Observer
|
|
583
|
-
|
|
584
|
-
```swift
|
|
585
|
-
@Observable
|
|
586
|
-
@MainActor
|
|
587
|
-
final class MemoryMonitor {
|
|
588
|
-
var isUnderPressure = false
|
|
589
|
-
|
|
590
|
-
init() {
|
|
591
|
-
NotificationCenter.default.addObserver(
|
|
592
|
-
forName: UIApplication.didReceiveMemoryWarningNotification,
|
|
593
|
-
object: nil,
|
|
594
|
-
queue: .main
|
|
595
|
-
) { [weak self] _ in
|
|
596
|
-
self?.handleMemoryWarning()
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
private func handleMemoryWarning() {
|
|
601
|
-
isUnderPressure = true
|
|
602
|
-
|
|
603
|
-
Task {
|
|
604
|
-
await ImageCache.shared.removeAll()
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Reset after a delay
|
|
608
|
-
Task {
|
|
609
|
-
try? await Task.sleep(for: .seconds(10))
|
|
610
|
-
isUnderPressure = false
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
### Process Memory Usage
|
|
617
|
-
|
|
618
|
-
```swift
|
|
619
|
-
func currentMemoryUsageMB() -> Double {
|
|
620
|
-
var info = mach_task_basic_info()
|
|
621
|
-
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
|
|
622
|
-
let result = withUnsafeMutablePointer(to: &info) {
|
|
623
|
-
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
|
624
|
-
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
guard result == KERN_SUCCESS else { return 0 }
|
|
628
|
-
return Double(info.resident_size) / (1024 * 1024)
|
|
629
|
-
}
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
### Adaptive Cache Sizing
|
|
633
|
-
|
|
634
|
-
```swift
|
|
635
|
-
actor AdaptiveImageCache {
|
|
636
|
-
private let cache = NSCache<NSString, UIImage>()
|
|
637
|
-
|
|
638
|
-
init() {
|
|
639
|
-
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
|
640
|
-
// Use at most 10% of physical memory for image cache
|
|
641
|
-
let budgetBytes = Int(totalMemory / 10)
|
|
642
|
-
cache.totalCostLimit = budgetBytes
|
|
643
|
-
cache.countLimit = 200
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
func image(for url: URL) -> UIImage? {
|
|
647
|
-
cache.object(forKey: url.absoluteString as NSString)
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
func store(_ image: UIImage, for url: URL) {
|
|
651
|
-
let cost = cgImageMemorySize(image)
|
|
652
|
-
cache.setObject(image, forKey: url.absoluteString as NSString, cost: cost)
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
/// Estimate the decoded bitmap size in bytes.
|
|
656
|
-
private func cgImageMemorySize(_ image: UIImage) -> Int {
|
|
657
|
-
guard let cgImage = image.cgImage else { return 0 }
|
|
658
|
-
return cgImage.bytesPerRow * cgImage.height
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
func purge() {
|
|
662
|
-
cache.removeAllObjects()
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
---
|
|
668
|
-
|
|
669
|
-
## Complete Image Loading Pipeline
|
|
670
|
-
|
|
671
|
-
A production-ready pipeline that combines in-memory caching, disk caching via URLCache, downsampling, and request coalescing.
|
|
672
|
-
|
|
673
|
-
### ImageLoader Actor
|
|
674
|
-
|
|
675
|
-
```swift
|
|
676
|
-
import UIKit
|
|
677
|
-
import ImageIO
|
|
678
|
-
|
|
679
|
-
actor ImageLoader {
|
|
680
|
-
static let shared = ImageLoader()
|
|
681
|
-
|
|
682
|
-
private let memoryCache = NSCache<NSString, UIImage>()
|
|
683
|
-
private let session: URLSession
|
|
684
|
-
private var inFlightTasks: [URL: Task<UIImage?, Never>] = [:]
|
|
685
|
-
|
|
686
|
-
init() {
|
|
687
|
-
let config = URLSessionConfiguration.default
|
|
688
|
-
config.urlCache = URLCache(
|
|
689
|
-
memoryCapacity: 50 * 1024 * 1024,
|
|
690
|
-
diskCapacity: 200 * 1024 * 1024
|
|
691
|
-
)
|
|
692
|
-
config.requestCachePolicy = .returnCacheDataElseLoad
|
|
693
|
-
config.httpMaximumConnectionsPerHost = 6
|
|
694
|
-
session = URLSession(configuration: config)
|
|
695
|
-
|
|
696
|
-
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
|
697
|
-
memoryCache.totalCostLimit = Int(totalMemory / 10)
|
|
698
|
-
memoryCache.countLimit = 200
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
/// Load an image, optionally downsampling to the given display size.
|
|
702
|
-
func load(from url: URL, displaySize: CGSize? = nil) async -> UIImage? {
|
|
703
|
-
let cacheKey = cacheKey(url: url, size: displaySize)
|
|
704
|
-
|
|
705
|
-
// 1. Check memory cache
|
|
706
|
-
if let cached = memoryCache.object(forKey: cacheKey as NSString) {
|
|
707
|
-
return cached
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// 2. Coalesce duplicate in-flight requests
|
|
711
|
-
if let existing = inFlightTasks[url] {
|
|
712
|
-
return await existing.value
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
let task = Task<UIImage?, Never> {
|
|
716
|
-
defer { inFlightTasks[url] = nil }
|
|
717
|
-
|
|
718
|
-
guard let (data, response) = try? await session.data(from: url),
|
|
719
|
-
let httpResponse = response as? HTTPURLResponse,
|
|
720
|
-
httpResponse.statusCode == 200 else {
|
|
721
|
-
return nil
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// 3. Downsample if a display size is provided
|
|
725
|
-
let image: UIImage?
|
|
726
|
-
if let displaySize {
|
|
727
|
-
image = Self.downsample(data: data, to: displaySize)
|
|
728
|
-
} else {
|
|
729
|
-
image = UIImage(data: data)
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// 4. Store in memory cache
|
|
733
|
-
if let image {
|
|
734
|
-
let cost = Self.bitmapSize(of: image)
|
|
735
|
-
memoryCache.setObject(image, forKey: cacheKey as NSString, cost: cost)
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
return image
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
inFlightTasks[url] = task
|
|
742
|
-
return await task.value
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
/// Remove a specific URL from the memory cache.
|
|
746
|
-
func evict(url: URL, displaySize: CGSize? = nil) {
|
|
747
|
-
let key = cacheKey(url: url, size: displaySize)
|
|
748
|
-
memoryCache.removeObject(forKey: key as NSString)
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/// Purge all in-memory cached images.
|
|
752
|
-
func purgeMemoryCache() {
|
|
753
|
-
memoryCache.removeAllObjects()
|
|
754
|
-
inFlightTasks.values.forEach { $0.cancel() }
|
|
755
|
-
inFlightTasks.removeAll()
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
// MARK: - Private
|
|
759
|
-
|
|
760
|
-
private func cacheKey(url: URL, size: CGSize?) -> String {
|
|
761
|
-
if let size {
|
|
762
|
-
return "\(url.absoluteString)_\(Int(size.width))x\(Int(size.height))"
|
|
763
|
-
}
|
|
764
|
-
return url.absoluteString
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
private static func downsample(data: Data, to pointSize: CGSize) -> UIImage? {
|
|
768
|
-
let scale = UITraitCollection.current.displayScale
|
|
769
|
-
let maxDimension = max(pointSize.width, pointSize.height) * scale
|
|
770
|
-
|
|
771
|
-
let options: [CFString: Any] = [
|
|
772
|
-
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
773
|
-
kCGImageSourceShouldCacheImmediately: true,
|
|
774
|
-
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
775
|
-
kCGImageSourceThumbnailMaxPixelSize: maxDimension
|
|
776
|
-
]
|
|
777
|
-
|
|
778
|
-
guard let source = CGImageSourceCreateWithData(data as CFData, nil),
|
|
779
|
-
let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
|
|
780
|
-
return nil
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
return UIImage(cgImage: cgImage)
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
private static func bitmapSize(of image: UIImage) -> Int {
|
|
787
|
-
guard let cgImage = image.cgImage else { return 0 }
|
|
788
|
-
return cgImage.bytesPerRow * cgImage.height
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
```
|
|
792
|
-
|
|
793
|
-
### SwiftUI View Using the Pipeline
|
|
794
|
-
|
|
795
|
-
```swift
|
|
796
|
-
struct PipelineImageView: View {
|
|
797
|
-
let url: URL
|
|
798
|
-
var displaySize: CGSize = CGSize(width: 300, height: 300)
|
|
799
|
-
|
|
800
|
-
@State private var image: UIImage?
|
|
801
|
-
@State private var isLoading = true
|
|
802
|
-
|
|
803
|
-
var body: some View {
|
|
804
|
-
ZStack {
|
|
805
|
-
if let image {
|
|
806
|
-
Image(uiImage: image)
|
|
807
|
-
.resizable()
|
|
808
|
-
.aspectRatio(contentMode: .fill)
|
|
809
|
-
} else if isLoading {
|
|
810
|
-
Color.secondary.opacity(0.15)
|
|
811
|
-
.overlay(ProgressView())
|
|
812
|
-
} else {
|
|
813
|
-
Color.secondary.opacity(0.15)
|
|
814
|
-
.overlay {
|
|
815
|
-
Image(systemName: "photo")
|
|
816
|
-
.font(.title2)
|
|
817
|
-
.foregroundStyle(.secondary)
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
.frame(width: displaySize.width, height: displaySize.height)
|
|
822
|
-
.clipped()
|
|
823
|
-
.task(id: url) {
|
|
824
|
-
isLoading = true
|
|
825
|
-
image = await ImageLoader.shared.load(from: url, displaySize: displaySize)
|
|
826
|
-
isLoading = false
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
```
|
|
831
|
-
|
|
832
|
-
### Memory Warning Integration
|
|
833
|
-
|
|
834
|
-
```swift
|
|
835
|
-
struct PhotoGridView: View {
|
|
836
|
-
let photos: [Photo]
|
|
837
|
-
|
|
838
|
-
var body: some View {
|
|
839
|
-
ScrollView {
|
|
840
|
-
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100), spacing: 2)], spacing: 2) {
|
|
841
|
-
ForEach(photos) { photo in
|
|
842
|
-
PipelineImageView(
|
|
843
|
-
url: photo.thumbnailURL,
|
|
844
|
-
displaySize: CGSize(width: 100, height: 100)
|
|
845
|
-
)
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
.onReceive(NotificationCenter.default.publisher(
|
|
850
|
-
for: UIApplication.didReceiveMemoryWarningNotification
|
|
851
|
-
)) { _ in
|
|
852
|
-
Task {
|
|
853
|
-
await ImageLoader.shared.purgeMemoryCache()
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
```
|
|
859
|
-
|
|
860
|
-
### When to Use Each Layer
|
|
861
|
-
|
|
862
|
-
| Scenario | Recommended Approach |
|
|
863
|
-
|---|---|
|
|
864
|
-
| Simple profile avatar | AsyncImage |
|
|
865
|
-
| Photo grid with scrolling | ImageLoader + downsampling + prefetch |
|
|
866
|
-
| Offline-capable gallery | ImageLoader + URLCache disk caching |
|
|
867
|
-
| Chat message images | ImageLoader + in-memory cache |
|
|
868
|
-
| Full-resolution photo viewer | ImageLoader without downsampling |
|
|
869
|
-
| Thumbnail in a widget | Downsample at fetch time, store in app group |
|
|
1
|
+
# Image Loading and Caching Patterns
|
|
2
|
+
|
|
3
|
+
Complete patterns for efficient image handling in iOS apps, from basic AsyncImage usage through production-ready caching and loading pipelines. All patterns use modern Swift async/await and target iOS 26 with Swift 6.2, backward-compatible to iOS 16 unless noted.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
- AsyncImage Patterns
|
|
7
|
+
- NSCache-Based In-Memory Cache
|
|
8
|
+
- URLCache-Based Disk Caching
|
|
9
|
+
- Image Downsampling with CGImageSource
|
|
10
|
+
- Image Prefetching for Lists and Grids
|
|
11
|
+
- HEIF/HEIC Handling
|
|
12
|
+
- Compression Before Upload
|
|
13
|
+
- Memory Budget Management
|
|
14
|
+
- Complete Image Loading Pipeline
|
|
15
|
+
|
|
16
|
+
## AsyncImage Patterns
|
|
17
|
+
|
|
18
|
+
AsyncImage (iOS 15+) provides built-in async image loading from a URL. Suitable for simple cases but has significant limitations for production use.
|
|
19
|
+
|
|
20
|
+
### Basic Usage
|
|
21
|
+
|
|
22
|
+
```swift
|
|
23
|
+
AsyncImage(url: URL(string: "https://example.com/photo.jpg"))
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Phase Handling
|
|
27
|
+
|
|
28
|
+
```swift
|
|
29
|
+
AsyncImage(url: imageURL) { phase in
|
|
30
|
+
switch phase {
|
|
31
|
+
case .empty:
|
|
32
|
+
ProgressView()
|
|
33
|
+
.frame(width: 200, height: 200)
|
|
34
|
+
case .success(let image):
|
|
35
|
+
image
|
|
36
|
+
.resizable()
|
|
37
|
+
.aspectRatio(contentMode: .fill)
|
|
38
|
+
.frame(width: 200, height: 200)
|
|
39
|
+
.clipped()
|
|
40
|
+
case .failure:
|
|
41
|
+
Image(systemName: "photo")
|
|
42
|
+
.font(.largeTitle)
|
|
43
|
+
.foregroundStyle(.secondary)
|
|
44
|
+
.frame(width: 200, height: 200)
|
|
45
|
+
@unknown default:
|
|
46
|
+
EmptyView()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Custom Transition
|
|
52
|
+
|
|
53
|
+
```swift
|
|
54
|
+
AsyncImage(url: imageURL, transaction: Transaction(animation: .easeIn(duration: 0.3))) { phase in
|
|
55
|
+
switch phase {
|
|
56
|
+
case .success(let image):
|
|
57
|
+
image
|
|
58
|
+
.resizable()
|
|
59
|
+
.aspectRatio(contentMode: .fill)
|
|
60
|
+
.transition(.opacity)
|
|
61
|
+
default:
|
|
62
|
+
Color.secondary.opacity(0.2)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
.frame(width: 200, height: 200)
|
|
66
|
+
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Limitations
|
|
70
|
+
|
|
71
|
+
AsyncImage has several shortcomings for production apps:
|
|
72
|
+
|
|
73
|
+
- **No caching across redraws**: images re-download when the view is recreated.
|
|
74
|
+
- **No prefetching**: cannot load images ahead of scroll position.
|
|
75
|
+
- **No custom URLSession**: uses the shared session with no cache policy control.
|
|
76
|
+
- **No access to raw data**: cannot process, downsample, or persist the image data.
|
|
77
|
+
- **No cancellation control**: tied entirely to the view lifecycle.
|
|
78
|
+
|
|
79
|
+
For anything beyond simple, low-volume image display, use a custom loading pipeline.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## NSCache-Based In-Memory Cache
|
|
84
|
+
|
|
85
|
+
An actor-isolated in-memory image cache backed by NSCache. Thread-safe and automatically evicts entries under memory pressure.
|
|
86
|
+
|
|
87
|
+
```swift
|
|
88
|
+
import UIKit
|
|
89
|
+
|
|
90
|
+
actor ImageCache {
|
|
91
|
+
static let shared = ImageCache()
|
|
92
|
+
|
|
93
|
+
private let cache = NSCache<NSString, UIImage>()
|
|
94
|
+
private var inFlightTasks: [URL: Task<UIImage?, Never>] = [:]
|
|
95
|
+
|
|
96
|
+
init(countLimit: Int = 100, totalCostLimit: Int = 50 * 1024 * 1024) {
|
|
97
|
+
cache.countLimit = countLimit
|
|
98
|
+
cache.totalCostLimit = totalCostLimit // 50 MB default
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func image(for url: URL) -> UIImage? {
|
|
102
|
+
cache.object(forKey: url.absoluteString as NSString)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func store(_ image: UIImage, for url: URL) {
|
|
106
|
+
let cost = image.jpegData(compressionQuality: 1.0)?.count ?? 0
|
|
107
|
+
cache.setObject(image, forKey: url.absoluteString as NSString, cost: cost)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func removeImage(for url: URL) {
|
|
111
|
+
cache.removeObject(forKey: url.absoluteString as NSString)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func removeAll() {
|
|
115
|
+
cache.removeAllObjects()
|
|
116
|
+
inFlightTasks.removeAll()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Fetch an image with request coalescing. Multiple callers for the same URL
|
|
120
|
+
/// share a single network request.
|
|
121
|
+
func fetch(from url: URL, session: URLSession = .shared) async -> UIImage? {
|
|
122
|
+
// Return cached image immediately
|
|
123
|
+
if let cached = image(for: url) {
|
|
124
|
+
return cached
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Coalesce duplicate in-flight requests
|
|
128
|
+
if let existingTask = inFlightTasks[url] {
|
|
129
|
+
return await existingTask.value
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let task = Task<UIImage?, Never> {
|
|
133
|
+
defer { inFlightTasks[url] = nil }
|
|
134
|
+
|
|
135
|
+
guard let (data, response) = try? await session.data(from: url),
|
|
136
|
+
let httpResponse = response as? HTTPURLResponse,
|
|
137
|
+
httpResponse.statusCode == 200,
|
|
138
|
+
let image = UIImage(data: data) else {
|
|
139
|
+
return nil
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
store(image, for: url)
|
|
143
|
+
return image
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
inFlightTasks[url] = task
|
|
147
|
+
return await task.value
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Usage in SwiftUI
|
|
153
|
+
|
|
154
|
+
```swift
|
|
155
|
+
struct CachedImageView: View {
|
|
156
|
+
let url: URL
|
|
157
|
+
@State private var image: UIImage?
|
|
158
|
+
|
|
159
|
+
var body: some View {
|
|
160
|
+
Group {
|
|
161
|
+
if let image {
|
|
162
|
+
Image(uiImage: image)
|
|
163
|
+
.resizable()
|
|
164
|
+
.aspectRatio(contentMode: .fill)
|
|
165
|
+
} else {
|
|
166
|
+
Color.secondary.opacity(0.2)
|
|
167
|
+
.overlay(ProgressView())
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
.task(id: url) {
|
|
171
|
+
image = await ImageCache.shared.fetch(from: url)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## URLCache-Based Disk Caching
|
|
180
|
+
|
|
181
|
+
Configure URLCache for persistent image caching that survives app restarts. URLCache handles HTTP cache headers automatically.
|
|
182
|
+
|
|
183
|
+
### Configuring a Dedicated URLSession
|
|
184
|
+
|
|
185
|
+
```swift
|
|
186
|
+
enum ImageSessionConfiguration {
|
|
187
|
+
static func makeSession() -> URLSession {
|
|
188
|
+
let config = URLSessionConfiguration.default
|
|
189
|
+
|
|
190
|
+
// 50 MB memory / 200 MB disk
|
|
191
|
+
config.urlCache = URLCache(
|
|
192
|
+
memoryCapacity: 50 * 1024 * 1024,
|
|
193
|
+
diskCapacity: 200 * 1024 * 1024,
|
|
194
|
+
directory: cacheDirectory
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
config.requestCachePolicy = .returnCacheDataElseLoad
|
|
198
|
+
config.httpMaximumConnectionsPerHost = 6
|
|
199
|
+
|
|
200
|
+
return URLSession(configuration: config)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private static var cacheDirectory: URL {
|
|
204
|
+
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
|
205
|
+
.appendingPathComponent("ImageCache", isDirectory: true)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Cache-Aware Request
|
|
211
|
+
|
|
212
|
+
```swift
|
|
213
|
+
func cachedImageData(from url: URL, session: URLSession) async throws -> Data {
|
|
214
|
+
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
|
|
215
|
+
let (data, _) = try await session.data(for: request)
|
|
216
|
+
return data
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Force Refresh (Bypass Cache)
|
|
221
|
+
|
|
222
|
+
```swift
|
|
223
|
+
func refreshImageData(from url: URL, session: URLSession) async throws -> Data {
|
|
224
|
+
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
|
|
225
|
+
let (data, _) = try await session.data(for: request)
|
|
226
|
+
return data
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Cache Cleanup
|
|
231
|
+
|
|
232
|
+
```swift
|
|
233
|
+
func cleanImageCache(session: URLSession) {
|
|
234
|
+
session.configuration.urlCache?.removeAllCachedResponses()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
func removeCachedImage(for url: URL, session: URLSession) {
|
|
238
|
+
let request = URLRequest(url: url)
|
|
239
|
+
session.configuration.urlCache?.removeCachedResponse(for: request)
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Image Downsampling with CGImageSource
|
|
246
|
+
|
|
247
|
+
Loading a full-resolution image into memory then scaling it in the view wastes significant memory. Downsampling at decode time creates a smaller bitmap directly.
|
|
248
|
+
|
|
249
|
+
### Downsample Function
|
|
250
|
+
|
|
251
|
+
```swift
|
|
252
|
+
import ImageIO
|
|
253
|
+
import UIKit
|
|
254
|
+
|
|
255
|
+
func downsample(data: Data, to pointSize: CGSize, scale: CGFloat = UITraitCollection.current.displayScale) -> UIImage? {
|
|
256
|
+
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
|
|
257
|
+
|
|
258
|
+
let options: [CFString: Any] = [
|
|
259
|
+
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
260
|
+
kCGImageSourceShouldCacheImmediately: true,
|
|
261
|
+
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
262
|
+
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
guard let source = CGImageSourceCreateWithData(data as CFData, nil),
|
|
266
|
+
let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
|
|
267
|
+
return nil
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return UIImage(cgImage: cgImage)
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Downsample from URL
|
|
275
|
+
|
|
276
|
+
```swift
|
|
277
|
+
func downsample(url: URL, to pointSize: CGSize, scale: CGFloat = UITraitCollection.current.displayScale) -> UIImage? {
|
|
278
|
+
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
|
|
279
|
+
|
|
280
|
+
let options: [CFString: Any] = [
|
|
281
|
+
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
282
|
+
kCGImageSourceShouldCacheImmediately: true,
|
|
283
|
+
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
284
|
+
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
|
|
288
|
+
let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
|
|
289
|
+
return nil
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return UIImage(cgImage: cgImage)
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### When to Downsample
|
|
297
|
+
|
|
298
|
+
- Displaying thumbnails in a list or grid (a 4032x3024 photo for a 100x100 cell wastes ~46 MB per image).
|
|
299
|
+
- User-selected photos from the photo library.
|
|
300
|
+
- Images fetched from a server that are larger than the display size.
|
|
301
|
+
- Any time the source image dimensions exceed 2x the display point size.
|
|
302
|
+
|
|
303
|
+
### Memory Savings
|
|
304
|
+
|
|
305
|
+
| Source Size | Display Size | Without Downsampling | With Downsampling |
|
|
306
|
+
|---|---|---|---|
|
|
307
|
+
| 4032x3024 | 100x100 pt @3x | ~46 MB | ~0.35 MB |
|
|
308
|
+
| 4032x3024 | 300x300 pt @3x | ~46 MB | ~3.1 MB |
|
|
309
|
+
| 1920x1080 | 100x100 pt @3x | ~7.9 MB | ~0.35 MB |
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Image Prefetching for Lists and Grids
|
|
314
|
+
|
|
315
|
+
Prefetch images before they scroll into view. Works with both UICollectionView data source prefetching and SwiftUI List.
|
|
316
|
+
|
|
317
|
+
### Prefetch Coordinator
|
|
318
|
+
|
|
319
|
+
```swift
|
|
320
|
+
@Observable
|
|
321
|
+
@MainActor
|
|
322
|
+
final class ImagePrefetcher {
|
|
323
|
+
private let cache: ImageCache
|
|
324
|
+
private let session: URLSession
|
|
325
|
+
private var prefetchTasks: [URL: Task<Void, Never>] = [:]
|
|
326
|
+
|
|
327
|
+
init(cache: ImageCache = .shared,
|
|
328
|
+
session: URLSession = ImageSessionConfiguration.makeSession()) {
|
|
329
|
+
self.cache = cache
|
|
330
|
+
self.session = session
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
func prefetch(urls: [URL]) {
|
|
334
|
+
for url in urls {
|
|
335
|
+
guard prefetchTasks[url] == nil else { continue }
|
|
336
|
+
|
|
337
|
+
prefetchTasks[url] = Task {
|
|
338
|
+
_ = await cache.fetch(from: url, session: session)
|
|
339
|
+
prefetchTasks[url] = nil
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
func cancelPrefetch(urls: [URL]) {
|
|
345
|
+
for url in urls {
|
|
346
|
+
prefetchTasks[url]?.cancel()
|
|
347
|
+
prefetchTasks[url] = nil
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
func cancelAll() {
|
|
352
|
+
prefetchTasks.values.forEach { $0.cancel() }
|
|
353
|
+
prefetchTasks.removeAll()
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### SwiftUI Integration with ScrollView and LazyVGrid
|
|
359
|
+
|
|
360
|
+
```swift
|
|
361
|
+
struct PhotoGrid: View {
|
|
362
|
+
let photos: [Photo]
|
|
363
|
+
@State private var prefetcher = ImagePrefetcher()
|
|
364
|
+
|
|
365
|
+
private let columns = [GridItem(.adaptive(minimum: 100), spacing: 2)]
|
|
366
|
+
|
|
367
|
+
var body: some View {
|
|
368
|
+
ScrollView {
|
|
369
|
+
LazyVGrid(columns: columns, spacing: 2) {
|
|
370
|
+
ForEach(photos) { photo in
|
|
371
|
+
CachedImageView(url: photo.thumbnailURL)
|
|
372
|
+
.aspectRatio(1, contentMode: .fill)
|
|
373
|
+
.clipped()
|
|
374
|
+
.onAppear {
|
|
375
|
+
prefetchNearby(photo)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
.onDisappear {
|
|
381
|
+
prefetcher.cancelAll()
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private func prefetchNearby(_ photo: Photo) {
|
|
386
|
+
guard let index = photos.firstIndex(where: { $0.id == photo.id }) else { return }
|
|
387
|
+
let prefetchRange = (index + 1)..<min(index + 10, photos.count)
|
|
388
|
+
let urls = prefetchRange.map { photos[$0].thumbnailURL }
|
|
389
|
+
prefetcher.prefetch(urls: urls)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### UICollectionView Prefetching (UIKit Interop)
|
|
395
|
+
|
|
396
|
+
```swift
|
|
397
|
+
final class PhotoCollectionPrefetcher: NSObject, UICollectionViewDataSourcePrefetching {
|
|
398
|
+
private let prefetcher = ImagePrefetcher()
|
|
399
|
+
private let photos: [Photo]
|
|
400
|
+
|
|
401
|
+
init(photos: [Photo]) {
|
|
402
|
+
self.photos = photos
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
func collectionView(_ collectionView: UICollectionView,
|
|
406
|
+
prefetchItemsAt indexPaths: [IndexPath]) {
|
|
407
|
+
let urls = indexPaths.map { photos[$0.item].thumbnailURL }
|
|
408
|
+
prefetcher.prefetch(urls: urls)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
func collectionView(_ collectionView: UICollectionView,
|
|
412
|
+
cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
|
|
413
|
+
let urls = indexPaths.map { photos[$0.item].thumbnailURL }
|
|
414
|
+
prefetcher.cancelPrefetch(urls: urls)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## HEIF/HEIC Handling
|
|
422
|
+
|
|
423
|
+
HEIF (High Efficiency Image Format) is the default camera format on modern iPhones. Handle detection, display, and conversion.
|
|
424
|
+
|
|
425
|
+
### Detection
|
|
426
|
+
|
|
427
|
+
```swift
|
|
428
|
+
import UniformTypeIdentifiers
|
|
429
|
+
|
|
430
|
+
func isHEIF(data: Data) -> Bool {
|
|
431
|
+
guard data.count >= 12 else { return false }
|
|
432
|
+
// Check for 'ftyp' box at byte 4
|
|
433
|
+
let ftypRange = data[4..<8]
|
|
434
|
+
return ftypRange.elementsEqual("ftyp".utf8)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
func isHEIF(url: URL) -> Bool {
|
|
438
|
+
guard let type = UTType(filenameExtension: url.pathExtension) else { return false }
|
|
439
|
+
return type.conforms(to: .heif) || type.conforms(to: .heic)
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Conversion to JPEG
|
|
444
|
+
|
|
445
|
+
```swift
|
|
446
|
+
func convertHEICToJPEG(data: Data, compressionQuality: CGFloat = 0.9) -> Data? {
|
|
447
|
+
guard let image = UIImage(data: data) else { return nil }
|
|
448
|
+
return image.jpegData(compressionQuality: compressionQuality)
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Conversion to PNG
|
|
453
|
+
|
|
454
|
+
```swift
|
|
455
|
+
func convertHEICToPNG(data: Data) -> Data? {
|
|
456
|
+
guard let image = UIImage(data: data) else { return nil }
|
|
457
|
+
return image.pngData()
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Conversion with CGImageDestination (More Control)
|
|
462
|
+
|
|
463
|
+
```swift
|
|
464
|
+
import ImageIO
|
|
465
|
+
|
|
466
|
+
func convertHEICToJPEG(sourceData: Data,
|
|
467
|
+
quality: CGFloat = 0.9,
|
|
468
|
+
preserveMetadata: Bool = true) -> Data? {
|
|
469
|
+
guard let source = CGImageSourceCreateWithData(sourceData as CFData, nil),
|
|
470
|
+
let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else {
|
|
471
|
+
return nil
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let mutableData = NSMutableData()
|
|
475
|
+
guard let destination = CGImageDestinationCreateWithData(
|
|
476
|
+
mutableData, UTType.jpeg.identifier as CFString, 1, nil
|
|
477
|
+
) else {
|
|
478
|
+
return nil
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
var options: [CFString: Any] = [
|
|
482
|
+
kCGImageDestinationLossyCompressionQuality: quality
|
|
483
|
+
]
|
|
484
|
+
|
|
485
|
+
// Preserve EXIF, GPS, and other metadata
|
|
486
|
+
if preserveMetadata,
|
|
487
|
+
let metadata = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) {
|
|
488
|
+
options[kCGImageDestinationMergeMetadata] = true
|
|
489
|
+
CGImageDestinationAddImage(destination, cgImage, metadata)
|
|
490
|
+
} else {
|
|
491
|
+
CGImageDestinationAddImage(destination, cgImage, options as CFDictionary)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
guard CGImageDestinationFinalize(destination) else { return nil }
|
|
495
|
+
return mutableData as Data
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## Compression Before Upload
|
|
502
|
+
|
|
503
|
+
Reduce file size before uploading to a server. Balance quality and size based on the use case.
|
|
504
|
+
|
|
505
|
+
### JPEG Compression with Target Size
|
|
506
|
+
|
|
507
|
+
```swift
|
|
508
|
+
func compressForUpload(image: UIImage,
|
|
509
|
+
maxBytes: Int = 1_000_000,
|
|
510
|
+
initialQuality: CGFloat = 0.9) -> Data? {
|
|
511
|
+
var quality = initialQuality
|
|
512
|
+
|
|
513
|
+
while quality > 0.1 {
|
|
514
|
+
guard let data = image.jpegData(compressionQuality: quality) else { return nil }
|
|
515
|
+
if data.count <= maxBytes {
|
|
516
|
+
return data
|
|
517
|
+
}
|
|
518
|
+
quality -= 0.1
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Final attempt at minimum quality
|
|
522
|
+
return image.jpegData(compressionQuality: 0.1)
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Resize and Compress
|
|
527
|
+
|
|
528
|
+
```swift
|
|
529
|
+
func resizeAndCompress(image: UIImage,
|
|
530
|
+
maxDimension: CGFloat = 1920,
|
|
531
|
+
compressionQuality: CGFloat = 0.8) -> Data? {
|
|
532
|
+
let size = image.size
|
|
533
|
+
let scale: CGFloat
|
|
534
|
+
|
|
535
|
+
if max(size.width, size.height) > maxDimension {
|
|
536
|
+
scale = maxDimension / max(size.width, size.height)
|
|
537
|
+
} else {
|
|
538
|
+
scale = 1.0
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
let newSize = CGSize(width: size.width * scale, height: size.height * scale)
|
|
542
|
+
|
|
543
|
+
let renderer = UIGraphicsImageRenderer(size: newSize)
|
|
544
|
+
let resized = renderer.image { _ in
|
|
545
|
+
image.draw(in: CGRect(origin: .zero, size: newSize))
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return resized.jpegData(compressionQuality: compressionQuality)
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### HEIF Compression (Smaller Files)
|
|
553
|
+
|
|
554
|
+
```swift
|
|
555
|
+
func compressAsHEIF(image: UIImage, quality: CGFloat = 0.8) -> Data? {
|
|
556
|
+
guard let cgImage = image.cgImage else { return nil }
|
|
557
|
+
|
|
558
|
+
let mutableData = NSMutableData()
|
|
559
|
+
guard let destination = CGImageDestinationCreateWithData(
|
|
560
|
+
mutableData, UTType.heic.identifier as CFString, 1, nil
|
|
561
|
+
) else {
|
|
562
|
+
return nil
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
let options: [CFString: Any] = [
|
|
566
|
+
kCGImageDestinationLossyCompressionQuality: quality
|
|
567
|
+
]
|
|
568
|
+
|
|
569
|
+
CGImageDestinationAddImage(destination, cgImage, options as CFDictionary)
|
|
570
|
+
guard CGImageDestinationFinalize(destination) else { return nil }
|
|
571
|
+
|
|
572
|
+
return mutableData as Data
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## Memory Budget Management
|
|
579
|
+
|
|
580
|
+
Monitor and respond to memory pressure to keep your image pipeline stable.
|
|
581
|
+
|
|
582
|
+
### Memory Warning Observer
|
|
583
|
+
|
|
584
|
+
```swift
|
|
585
|
+
@Observable
|
|
586
|
+
@MainActor
|
|
587
|
+
final class MemoryMonitor {
|
|
588
|
+
var isUnderPressure = false
|
|
589
|
+
|
|
590
|
+
init() {
|
|
591
|
+
NotificationCenter.default.addObserver(
|
|
592
|
+
forName: UIApplication.didReceiveMemoryWarningNotification,
|
|
593
|
+
object: nil,
|
|
594
|
+
queue: .main
|
|
595
|
+
) { [weak self] _ in
|
|
596
|
+
self?.handleMemoryWarning()
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private func handleMemoryWarning() {
|
|
601
|
+
isUnderPressure = true
|
|
602
|
+
|
|
603
|
+
Task {
|
|
604
|
+
await ImageCache.shared.removeAll()
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Reset after a delay
|
|
608
|
+
Task {
|
|
609
|
+
try? await Task.sleep(for: .seconds(10))
|
|
610
|
+
isUnderPressure = false
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### Process Memory Usage
|
|
617
|
+
|
|
618
|
+
```swift
|
|
619
|
+
func currentMemoryUsageMB() -> Double {
|
|
620
|
+
var info = mach_task_basic_info()
|
|
621
|
+
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
|
|
622
|
+
let result = withUnsafeMutablePointer(to: &info) {
|
|
623
|
+
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
|
624
|
+
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
guard result == KERN_SUCCESS else { return 0 }
|
|
628
|
+
return Double(info.resident_size) / (1024 * 1024)
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### Adaptive Cache Sizing
|
|
633
|
+
|
|
634
|
+
```swift
|
|
635
|
+
actor AdaptiveImageCache {
|
|
636
|
+
private let cache = NSCache<NSString, UIImage>()
|
|
637
|
+
|
|
638
|
+
init() {
|
|
639
|
+
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
|
640
|
+
// Use at most 10% of physical memory for image cache
|
|
641
|
+
let budgetBytes = Int(totalMemory / 10)
|
|
642
|
+
cache.totalCostLimit = budgetBytes
|
|
643
|
+
cache.countLimit = 200
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
func image(for url: URL) -> UIImage? {
|
|
647
|
+
cache.object(forKey: url.absoluteString as NSString)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
func store(_ image: UIImage, for url: URL) {
|
|
651
|
+
let cost = cgImageMemorySize(image)
|
|
652
|
+
cache.setObject(image, forKey: url.absoluteString as NSString, cost: cost)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/// Estimate the decoded bitmap size in bytes.
|
|
656
|
+
private func cgImageMemorySize(_ image: UIImage) -> Int {
|
|
657
|
+
guard let cgImage = image.cgImage else { return 0 }
|
|
658
|
+
return cgImage.bytesPerRow * cgImage.height
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
func purge() {
|
|
662
|
+
cache.removeAllObjects()
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
---
|
|
668
|
+
|
|
669
|
+
## Complete Image Loading Pipeline
|
|
670
|
+
|
|
671
|
+
A production-ready pipeline that combines in-memory caching, disk caching via URLCache, downsampling, and request coalescing.
|
|
672
|
+
|
|
673
|
+
### ImageLoader Actor
|
|
674
|
+
|
|
675
|
+
```swift
|
|
676
|
+
import UIKit
|
|
677
|
+
import ImageIO
|
|
678
|
+
|
|
679
|
+
actor ImageLoader {
|
|
680
|
+
static let shared = ImageLoader()
|
|
681
|
+
|
|
682
|
+
private let memoryCache = NSCache<NSString, UIImage>()
|
|
683
|
+
private let session: URLSession
|
|
684
|
+
private var inFlightTasks: [URL: Task<UIImage?, Never>] = [:]
|
|
685
|
+
|
|
686
|
+
init() {
|
|
687
|
+
let config = URLSessionConfiguration.default
|
|
688
|
+
config.urlCache = URLCache(
|
|
689
|
+
memoryCapacity: 50 * 1024 * 1024,
|
|
690
|
+
diskCapacity: 200 * 1024 * 1024
|
|
691
|
+
)
|
|
692
|
+
config.requestCachePolicy = .returnCacheDataElseLoad
|
|
693
|
+
config.httpMaximumConnectionsPerHost = 6
|
|
694
|
+
session = URLSession(configuration: config)
|
|
695
|
+
|
|
696
|
+
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
|
697
|
+
memoryCache.totalCostLimit = Int(totalMemory / 10)
|
|
698
|
+
memoryCache.countLimit = 200
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/// Load an image, optionally downsampling to the given display size.
|
|
702
|
+
func load(from url: URL, displaySize: CGSize? = nil) async -> UIImage? {
|
|
703
|
+
let cacheKey = cacheKey(url: url, size: displaySize)
|
|
704
|
+
|
|
705
|
+
// 1. Check memory cache
|
|
706
|
+
if let cached = memoryCache.object(forKey: cacheKey as NSString) {
|
|
707
|
+
return cached
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// 2. Coalesce duplicate in-flight requests
|
|
711
|
+
if let existing = inFlightTasks[url] {
|
|
712
|
+
return await existing.value
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
let task = Task<UIImage?, Never> {
|
|
716
|
+
defer { inFlightTasks[url] = nil }
|
|
717
|
+
|
|
718
|
+
guard let (data, response) = try? await session.data(from: url),
|
|
719
|
+
let httpResponse = response as? HTTPURLResponse,
|
|
720
|
+
httpResponse.statusCode == 200 else {
|
|
721
|
+
return nil
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// 3. Downsample if a display size is provided
|
|
725
|
+
let image: UIImage?
|
|
726
|
+
if let displaySize {
|
|
727
|
+
image = Self.downsample(data: data, to: displaySize)
|
|
728
|
+
} else {
|
|
729
|
+
image = UIImage(data: data)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// 4. Store in memory cache
|
|
733
|
+
if let image {
|
|
734
|
+
let cost = Self.bitmapSize(of: image)
|
|
735
|
+
memoryCache.setObject(image, forKey: cacheKey as NSString, cost: cost)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return image
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
inFlightTasks[url] = task
|
|
742
|
+
return await task.value
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/// Remove a specific URL from the memory cache.
|
|
746
|
+
func evict(url: URL, displaySize: CGSize? = nil) {
|
|
747
|
+
let key = cacheKey(url: url, size: displaySize)
|
|
748
|
+
memoryCache.removeObject(forKey: key as NSString)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/// Purge all in-memory cached images.
|
|
752
|
+
func purgeMemoryCache() {
|
|
753
|
+
memoryCache.removeAllObjects()
|
|
754
|
+
inFlightTasks.values.forEach { $0.cancel() }
|
|
755
|
+
inFlightTasks.removeAll()
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// MARK: - Private
|
|
759
|
+
|
|
760
|
+
private func cacheKey(url: URL, size: CGSize?) -> String {
|
|
761
|
+
if let size {
|
|
762
|
+
return "\(url.absoluteString)_\(Int(size.width))x\(Int(size.height))"
|
|
763
|
+
}
|
|
764
|
+
return url.absoluteString
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private static func downsample(data: Data, to pointSize: CGSize) -> UIImage? {
|
|
768
|
+
let scale = UITraitCollection.current.displayScale
|
|
769
|
+
let maxDimension = max(pointSize.width, pointSize.height) * scale
|
|
770
|
+
|
|
771
|
+
let options: [CFString: Any] = [
|
|
772
|
+
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
773
|
+
kCGImageSourceShouldCacheImmediately: true,
|
|
774
|
+
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
775
|
+
kCGImageSourceThumbnailMaxPixelSize: maxDimension
|
|
776
|
+
]
|
|
777
|
+
|
|
778
|
+
guard let source = CGImageSourceCreateWithData(data as CFData, nil),
|
|
779
|
+
let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
|
|
780
|
+
return nil
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return UIImage(cgImage: cgImage)
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
private static func bitmapSize(of image: UIImage) -> Int {
|
|
787
|
+
guard let cgImage = image.cgImage else { return 0 }
|
|
788
|
+
return cgImage.bytesPerRow * cgImage.height
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
### SwiftUI View Using the Pipeline
|
|
794
|
+
|
|
795
|
+
```swift
|
|
796
|
+
struct PipelineImageView: View {
|
|
797
|
+
let url: URL
|
|
798
|
+
var displaySize: CGSize = CGSize(width: 300, height: 300)
|
|
799
|
+
|
|
800
|
+
@State private var image: UIImage?
|
|
801
|
+
@State private var isLoading = true
|
|
802
|
+
|
|
803
|
+
var body: some View {
|
|
804
|
+
ZStack {
|
|
805
|
+
if let image {
|
|
806
|
+
Image(uiImage: image)
|
|
807
|
+
.resizable()
|
|
808
|
+
.aspectRatio(contentMode: .fill)
|
|
809
|
+
} else if isLoading {
|
|
810
|
+
Color.secondary.opacity(0.15)
|
|
811
|
+
.overlay(ProgressView())
|
|
812
|
+
} else {
|
|
813
|
+
Color.secondary.opacity(0.15)
|
|
814
|
+
.overlay {
|
|
815
|
+
Image(systemName: "photo")
|
|
816
|
+
.font(.title2)
|
|
817
|
+
.foregroundStyle(.secondary)
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
.frame(width: displaySize.width, height: displaySize.height)
|
|
822
|
+
.clipped()
|
|
823
|
+
.task(id: url) {
|
|
824
|
+
isLoading = true
|
|
825
|
+
image = await ImageLoader.shared.load(from: url, displaySize: displaySize)
|
|
826
|
+
isLoading = false
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
### Memory Warning Integration
|
|
833
|
+
|
|
834
|
+
```swift
|
|
835
|
+
struct PhotoGridView: View {
|
|
836
|
+
let photos: [Photo]
|
|
837
|
+
|
|
838
|
+
var body: some View {
|
|
839
|
+
ScrollView {
|
|
840
|
+
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100), spacing: 2)], spacing: 2) {
|
|
841
|
+
ForEach(photos) { photo in
|
|
842
|
+
PipelineImageView(
|
|
843
|
+
url: photo.thumbnailURL,
|
|
844
|
+
displaySize: CGSize(width: 100, height: 100)
|
|
845
|
+
)
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
.onReceive(NotificationCenter.default.publisher(
|
|
850
|
+
for: UIApplication.didReceiveMemoryWarningNotification
|
|
851
|
+
)) { _ in
|
|
852
|
+
Task {
|
|
853
|
+
await ImageLoader.shared.purgeMemoryCache()
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
### When to Use Each Layer
|
|
861
|
+
|
|
862
|
+
| Scenario | Recommended Approach |
|
|
863
|
+
|---|---|
|
|
864
|
+
| Simple profile avatar | AsyncImage |
|
|
865
|
+
| Photo grid with scrolling | ImageLoader + downsampling + prefetch |
|
|
866
|
+
| Offline-capable gallery | ImageLoader + URLCache disk caching |
|
|
867
|
+
| Chat message images | ImageLoader + in-memory cache |
|
|
868
|
+
| Full-resolution photo viewer | ImageLoader without downsampling |
|
|
869
|
+
| Thumbnail in a widget | Downsample at fetch time, store in app group |
|