$npx -y skills add rshankras/claude-code-apple-skills --skill image-loadingGenerates an image loading pipeline with memory/disk caching, deduplication, and a CachedAsyncImage SwiftUI view. Use when user wants image caching, lazy image loading, or a replacement for AsyncImage.
| 1 | # Image Loading Generator |
| 2 | |
| 3 | Generate a production image loading pipeline with NSCache memory cache, LRU disk cache, request deduplication, image processing, and a drop-in `CachedAsyncImage` SwiftUI view. |
| 4 | |
| 5 | ## When This Skill Activates |
| 6 | |
| 7 | Use this skill when the user: |
| 8 | - Asks to "add image caching" or "cache images" |
| 9 | - Wants to "replace AsyncImage" or fix "AsyncImage has no cache" |
| 10 | - Mentions "image loading pipeline" or "lazy image loading" |
| 11 | - Asks about "image download" or "image prefetching" |
| 12 | - Wants "thumbnail generation" or "image resizing" |
| 13 | |
| 14 | ## Pre-Generation Checks |
| 15 | |
| 16 | ### 1. Project Context Detection |
| 17 | - [ ] Check Swift version (requires Swift 5.9+) |
| 18 | - [ ] Check deployment target (iOS 16+ / macOS 13+) |
| 19 | - [ ] Check for @Observable support (iOS 17+ / macOS 14+) |
| 20 | - [ ] Identify source file locations |
| 21 | |
| 22 | ### 2. Conflict Detection |
| 23 | Search for existing image loading: |
| 24 | ``` |
| 25 | Glob: **/*ImageCache*.swift, **/*ImageLoader*.swift, **/*ImagePipeline*.swift |
| 26 | Grep: "AsyncImage" or "UIImage" or "NSImage" or "ImageCache" |
| 27 | ``` |
| 28 | |
| 29 | If third-party library found (Kingfisher, SDWebImage, Nuke): |
| 30 | - Ask if user wants to replace or keep it |
| 31 | - If keeping, don't generate — advise on best practices instead |
| 32 | |
| 33 | ### 3. Platform Detection |
| 34 | Determine if generating for iOS (UIImage) or macOS (NSImage) or both (cross-platform typealias). |
| 35 | |
| 36 | ## Configuration Questions |
| 37 | |
| 38 | Ask user via AskUserQuestion: |
| 39 | |
| 40 | 1. **Cache sizes?** |
| 41 | - Small (50 MB memory / 100 MB disk) |
| 42 | - Medium (100 MB memory / 250 MB disk) — recommended |
| 43 | - Large (200 MB memory / 500 MB disk) |
| 44 | |
| 45 | 2. **Image processing?** |
| 46 | - Resize to fit (downscale large images to save memory) |
| 47 | - Thumbnail generation (create small thumbnails for lists) |
| 48 | - None (cache original images only) |
| 49 | |
| 50 | 3. **Additional features?** (multi-select) |
| 51 | - Prefetching for collections (preload images for visible rows + buffer) |
| 52 | - Placeholder and error images |
| 53 | - Progress indicator during download |
| 54 | |
| 55 | 4. **Platform?** |
| 56 | - iOS only |
| 57 | - macOS only |
| 58 | - Cross-platform (iOS + macOS) |
| 59 | |
| 60 | ## Generation Process |
| 61 | |
| 62 | ### Step 1: Read Templates |
| 63 | Read `image-loading-patterns.md` for architecture guidance. |
| 64 | Read `templates.md` for production Swift code. |
| 65 | |
| 66 | ### Step 2: Create Core Files |
| 67 | Generate these files: |
| 68 | 1. `ImageCache.swift` — Protocol for cache interface |
| 69 | 2. `MemoryImageCache.swift` — NSCache-based with configurable size |
| 70 | 3. `DiskImageCache.swift` — FileManager LRU with expiration |
| 71 | 4. `ImageDownloader.swift` — Actor-based with deduplication + cancellation |
| 72 | 5. `ImagePipeline.swift` — Orchestrator (cache → download → process → store) |
| 73 | |
| 74 | ### Step 3: Create UI Files |
| 75 | 6. `CachedAsyncImage.swift` — Drop-in SwiftUI view replacement |
| 76 | |
| 77 | ### Step 4: Create Optional Files |
| 78 | Based on configuration: |
| 79 | - `ImageProcessor.swift` — If resize or thumbnail selected |
| 80 | - `ImagePrefetcher.swift` — If prefetching selected |
| 81 | |
| 82 | ### Step 5: Determine File Location |
| 83 | Check project structure: |
| 84 | - If `Sources/` exists → `Sources/ImageLoading/` |
| 85 | - If `App/` exists → `App/ImageLoading/` |
| 86 | - Otherwise → `ImageLoading/` |
| 87 | |
| 88 | ## Output Format |
| 89 | |
| 90 | After generation, provide: |
| 91 | |
| 92 | ### Files Created |
| 93 | ``` |
| 94 | ImageLoading/ |
| 95 | ├── ImageCache.swift # Protocol for cache interface |
| 96 | ├── MemoryImageCache.swift # NSCache-based memory cache |
| 97 | ├── DiskImageCache.swift # LRU disk cache with expiration |
| 98 | ├── ImageDownloader.swift # Actor-based downloader |
| 99 | ├── ImagePipeline.swift # Orchestrator |
| 100 | ├── ImageProcessor.swift # Resize, thumbnails (optional) |
| 101 | ├── CachedAsyncImage.swift # SwiftUI view |
| 102 | └── ImagePrefetcher.swift # Collection prefetching (optional) |
| 103 | ``` |
| 104 | |
| 105 | ### Integration Steps |
| 106 | |
| 107 | **Drop-in replacement for AsyncImage:** |
| 108 | ```swift |
| 109 | // Before (no caching) |
| 110 | AsyncImage(url: user.avatarURL) { image in |
| 111 | image.resizable().aspectRatio(contentMode: .fill) |
| 112 | } placeholder: { |
| 113 | ProgressView() |
| 114 | } |
| 115 | |
| 116 | // After (with caching) |
| 117 | CachedAsyncImage(url: user.avatarURL) { image in |
| 118 | image.resizable().aspectRatio(contentMode: .fill) |
| 119 | } placeholder: { |
| 120 | ProgressView() |
| 121 | } |
| 122 | ``` |
| 123 | |
| 124 | **In a List:** |
| 125 | ```swift |
| 126 | List(users) { user in |
| 127 | HStack { |
| 128 | CachedAsyncImage(url: user.avatarURL) { image in |
| 129 | image.resizable().frame(width: 44, height: 44).clipShape(Circle()) |
| 130 | } placeholder: { |
| 131 | Circle().fill(Color.secondary.opacity(0.2)).frame(width: 44, height: 44) |
| 132 | } |
| 133 | Text(user.name) |
| 134 | } |
| 135 | } |
| 136 | ``` |
| 137 | |
| 138 | **With prefetching:** |
| 139 | ```swift |
| 140 | struct UsersListView: View { |
| 141 | let users: [User] |
| 142 | @State private var prefetcher = ImagePrefetcher() |
| 143 | |
| 144 | var body: some View { |
| 145 | List(users) { user in |
| 146 | UserRow(user: user) |
| 147 | .onAppear { prefetcher.startPrefetching(urls: nearbyURLs(for: user)) } |
| 148 | .onDisappear { prefe |