All skills
rshankras avatar

/image-loading

@32566aa

Generates 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.

Use this Skill: https://skilld.dev/gh/rshankras/claude-code-apple-skills/image-loading

This session only. Nothing lands on disk.

templates.md

≈4.7k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Image Loading Code Templates

Production-ready Swift templates for image loading pipeline. All code targets iOS 16+ / macOS 13+ (iOS 17+ / macOS 14+ for @Observable) and uses modern Swift concurrency.

Platform Compatibility

#if canImport(UIKit)
import UIKit
typealias PlatformImage = UIImage
#elseif canImport(AppKit)
import AppKit
typealias PlatformImage = NSImage
#endif

ImageCache.swift

import Foundation

/// Protocol for image cache storage.
///
/// Implementations handle memory and/or disk caching.
protocol ImageCaching: Sendable {
    func image(for url: URL) async -> PlatformImage?
    func store(_ image: PlatformImage, for url: URL) async
    func remove(for url: URL) async
    func removeAll() async
}

MemoryImageCache.swift

import Foundation

/// NSCache-based memory image cache.
///
/// - Auto-evicts under memory pressure (NSCache behavior)
/// - Thread-safe without manual locking
/// - Configurable count and cost limits
final class MemoryImageCache: ImageCaching, @unchecked Sendable {
    private let cache = NSCache<NSString, PlatformImage>()

    init(countLimit: Int = 200, totalCostLimit: Int = 100 * 1024 * 1024) {
        cache.countLimit = countLimit
        cache.totalCostLimit = totalCostLimit
    }

    func image(for url: URL) async -> PlatformImage? {
        cache.object(forKey: url.absoluteString as NSString)
    }

    func store(_ image: PlatformImage, for url: URL) async {
        let cost = estimatedCost(of: image)
        cache.setObject(image, forKey: url.absoluteString as NSString, cost: cost)
    }

    func remove(for url: URL) async {
        cache.removeObject(forKey: url.absoluteString as NSString)
    }

    func removeAll() async {
        cache.removeAllObjects()
    }

    private func estimatedCost(of image: PlatformImage) -> Int {
        #if canImport(UIKit)
        guard let cgImage = image.cgImage else { return 0 }
        return cgImage.bytesPerRow * cgImage.height
        #elseif canImport(AppKit)
        guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return 0 }
        return cgImage.bytesPerRow * cgImage.height
        #endif
    }
}

DiskImageCache.swift

import Foundation
import CryptoKit

/// Disk-backed image cache with LRU eviction and TTL expiration.
///
/// Stores images as JPEG files in the Caches directory.
/// Uses SHA-256 hashed filenames to avoid path conflicts.
actor DiskImageCache: ImageCaching {
    private let cacheDirectory: URL
    private let maxDiskSize: Int
    private let maxAge: TimeInterval

    init(
        directoryName: String = "ImageCache",
        maxDiskSize: Int = 250 * 1024 * 1024,
        maxAge: TimeInterval = 7 * 24 * 3600  // 7 days
    ) {
        self.cacheDirectory = FileManager.default
            .urls(for: .cachesDirectory, in: .userDomainMask)[0]
            .appendingPathComponent(directoryName, isDirectory: true)
        self.maxDiskSize = maxDiskSize
        self.maxAge = maxAge

        try? FileManager.default.createDirectory(
            at: cacheDirectory,
            withIntermediateDirectories: true
        )
    }

    func image(for url: URL) async -> PlatformImage? {
        let fileURL = fileURL(for: url)
        guard FileManager.default.fileExists(atPath: fileURL.path) else { return nil }

        // Check expiration
        if let attrs = try? FileManager.default.attributesOfItem(atPath: fileURL.path),
           let modified = attrs[.modificationDate] as? Date,
           Date().timeIntervalSince(modified) > maxAge {
            try? FileManager.default.removeItem(at: fileURL)
            return nil
        }

        // Touch access date for LRU
        try? FileManager.default.setAttributes(
            [.modificationDate: Date()],
            ofItemAtPath: fileURL.path
        )

        guard let data = try? Data(contentsOf: fileURL) else { return nil }
        return PlatformImage(data: data)
    }

    func store(_ image: PlatformImage, for url: URL) async {
        let fileURL = fileURL(for: url)

        #if canImport(UIKit)
        guard let data = image.jpegData(compressionQuality: 0.8) else { return }
        #elseif canImport(AppKit)
        guard let tiffData = image.tiffRepresentation,
              let bitmap = NSBitmapImageRep(data: tiffData),
              let data = bitmap.representation(using: .jpeg, properties: [.compressionFactor: 0.8]) else { return }
        #endif

        try? data.write(to: fileURL)
        await evictIfNeeded()
    }

    func remove(for url: URL) async {
        let fileURL = fileURL(for: url)
        try? FileManager.default.removeItem(at: fileURL)
    }

    func removeAll() async {
        try? FileManager.default.removeItem(at: cacheDirectory)
        try? FileManager.default.createDirectory(
            at: cacheDirectory,
            withIntermediateDirectories: true
        )
    }

    // MARK: - Private

    private func fileURL(for url: URL) -> URL {
        let hash = SHA256.hash(data: Data(url.absoluteString.utf8))
        let filename = hash.compactMap { String(format: "%02x", $0) }.joined()
        return cacheDirectory.appendingPathComponent(filename)
    }

    private func evictIfNeeded() async {
        let fileManager = FileManager.default
        guard let files = try? fileManager.contentsOfDirectory(
            at: cacheDirectory,
            includingPropertiesForKeys: [.fileSizeKey, .contentModificationDateKey],
            options: .skipsHiddenFiles
        ) else { return }

        let totalSize = files.reduce(0) { total, url in
            let size = (try? url.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? 0
            return total + size
        }

        guard totalSize > maxDiskSize else { return }

        // LRU: sort by modification date (oldest first)
        let sorted = files.sorted { a, b in
            let dateA = (try? a.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? .distantPast
            let dateB = (try? b.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? .distantPast
            return dateA < dateB
        }

        var currentSize = totalSize
        for url in sorted {
            guard currentSize > maxDiskSize else { break }
            let size = (try? url.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? 0
            try? fileManager.removeItem(at: url)
            currentSize -= size
        }
    }
}

ImageDownloader.swift

import Foundation

/// Protocol for image downloading.
protocol ImageDownloading: Actor {
    func download(url: URL) async throws -> Data
    func cancel(url: URL)
}

/// Actor-based image downloader with request deduplication.
///
/// If multiple views request the same URL simultaneously,
/// only one download occurs and all callers receive the result.
actor ImageDownloader: ImageDownloading {
    private var activeDownloads: [URL: Task<Data, Error>] = [:]
    private let session: URLSession

    init(session: URLSession? = nil) {
        let config = URLSessionConfiguration.default
        config.urlCache = nil  // We handle caching ourselves
        self.session = session ?? URLSession(configuration: config)
    }

    func download(url: URL) async throws -> Data {
        // Deduplicate: if already downloading, wait for existing task
        if let existing = activeDownloads[url] {
            return try await existing.value
        }

        let task = Task<Data, Error> {
            let (data, response) = try await session.data(from: url)

            guard let httpResponse = response as? HTTPURLResponse,
                  (200...299).contains(httpResponse.statusCode) else {
                throw ImageLoadingError.downloadFailed(url: url)
            }

            return data
        }

        activeDownloads[url] = task

        do {
            let data = try await task.value
            activeDownloads.removeValue(forKey: url)
            return data
        } catch {
            activeDownloads.removeValue(forKey: url)
            throw error
        }
    }

    func cancel(url: URL) {
        activeDownloads[url]?.cancel()
        activeDownloads.removeValue(forKey: url)
    }
}

/// Errors specific to image loading.
enum ImageLoadingError: Error, LocalizedError {
    case downloadFailed(url: URL)
    case decodingFailed(url: URL)
    case processingFailed

    var errorDescription: String? {
        switch self {
        case .downloadFailed(let url):
            return "Failed to download image from \(url.absoluteString)"
        case .decodingFailed(let url):
            return "Failed to decode image from \(url.absoluteString)"
        case .processingFailed:
            return "Failed to process image"
        }
    }
}

ImagePipeline.swift

import Foundation
import SwiftUI

/// Central image loading orchestrator.
///
/// Flow: Memory Cache → Disk Cache → Download → Process → Store
///
/// Usage:
/// ```swift
/// let pipeline = ImagePipeline.shared
/// let image = try await pipeline.image(for: url)
/// ```
@Observable
final class ImagePipeline: @unchecked Sendable {
    static let shared = ImagePipeline()

    private let memoryCache: MemoryImageCache
    private let diskCache: DiskImageCache
    private let downloader: any ImageDownloading
    private let processor: ImageProcessor?

    init(
        memoryCache: MemoryImageCache = MemoryImageCache(),
        diskCache: DiskImageCache = DiskImageCache(),
        downloader: any ImageDownloading = ImageDownloader(),
        processor: ImageProcessor? = nil
    ) {
        self.memoryCache = memoryCache
        self.diskCache = diskCache
        self.downloader = downloader
        self.processor = processor
    }

    /// Load an image from cache or network.
    func image(for url: URL, processing: ImageProcessingOptions? = nil) async throws -> PlatformImage {
        let cacheKey = processing.map { "\(url.absoluteString)_\($0.cacheKeySuffix)" } ?? url.absoluteString
        let cacheURL = URL(string: cacheKey) ?? url

        // 1. Check memory cache
        if let cached = await memoryCache.image(for: cacheURL) {
            return cached
        }

        // 2. Check disk cache
        if let cached = await diskCache.image(for: cacheURL) {
            await memoryCache.store(cached, for: cacheURL)
            return cached
        }

        // 3. Download
        let data = try await downloader.download(url: url)

        guard var image = PlatformImage(data: data) else {
            throw ImageLoadingError.decodingFailed(url: url)
        }

        // 4. Process (optional)
        if let processing, let processor {
            image = try processor.process(image, options: processing)
        }

        // 5. Store in both caches
        await memoryCache.store(image, for: cacheURL)
        await diskCache.store(image, for: cacheURL)

        return image
    }

    /// Cancel a pending download.
    func cancel(for url: URL) async {
        await downloader.cancel(url: url)
    }

    /// Clear all caches.
    func clearCache() async {
        await memoryCache.removeAll()
        await diskCache.removeAll()
    }
}

// For testing: allow injection via SwiftUI Environment
private struct ImagePipelineKey: EnvironmentKey {
    static let defaultValue: ImagePipeline = .shared
}

extension EnvironmentValues {
    var imagePipeline: ImagePipeline {
        get { self[ImagePipelineKey.self] }
        set { self[ImagePipelineKey.self] = newValue }
    }
}

ImageProcessor.swift

import Foundation
import CoreGraphics
import ImageIO

/// Image processing options for the pipeline.
enum ImageProcessingOptions: Sendable {
    /// Resize to fit within target size, maintaining aspect ratio.
    case resize(targetSize: CGSize)

    /// Generate a thumbnail of the specified max pixel dimension.
    case thumbnail(maxPixelSize: Int)

    /// Cache key suffix to differentiate processed variants.
    var cacheKeySuffix: String {
        switch self {
        case .resize(let size):
            return "resize_\(Int(size.width))x\(Int(size.height))"
        case .thumbnail(let size):
            return "thumb_\(size)"
        }
    }
}

/// Handles image processing (resize, thumbnail generation).
struct ImageProcessor: Sendable {

    func process(_ image: PlatformImage, options: ImageProcessingOptions) throws -> PlatformImage {
        switch options {
        case .resize(let targetSize):
            return try resize(image, to: targetSize)
        case .thumbnail(let maxPixelSize):
            return try generateThumbnail(from: image, maxPixelSize: maxPixelSize)
        }
    }

    // MARK: - Resize

    private func resize(_ image: PlatformImage, to targetSize: CGSize) throws -> PlatformImage {
        #if canImport(UIKit)
        let renderer = UIGraphicsImageRenderer(size: targetSize)
        return renderer.image { _ in
            image.draw(in: CGRect(origin: .zero, size: targetSize))
        }
        #elseif canImport(AppKit)
        let newImage = NSImage(size: targetSize)
        newImage.lockFocus()
        image.draw(
            in: NSRect(origin: .zero, size: targetSize),
            from: NSRect(origin: .zero, size: image.size),
            operation: .copy,
            fraction: 1.0
        )
        newImage.unlockFocus()
        return newImage
        #endif
    }

    // MARK: - Thumbnail (ImageIO — most efficient)

    private func generateThumbnail(from image: PlatformImage, maxPixelSize: Int) throws -> PlatformImage {
        #if canImport(UIKit)
        guard let data = image.jpegData(compressionQuality: 0.9) else {
            throw ImageLoadingError.processingFailed
        }
        #elseif canImport(AppKit)
        guard let tiffData = image.tiffRepresentation,
              let bitmap = NSBitmapImageRep(data: tiffData),
              let data = bitmap.representation(using: .jpeg, properties: [.compressionFactor: 0.9]) else {
            throw ImageLoadingError.processingFailed
        }
        #endif

        let options: [CFString: Any] = [
            kCGImageSourceCreateThumbnailFromImageAlways: true,
            kCGImageSourceThumbnailMaxPixelSize: maxPixelSize,
            kCGImageSourceCreateThumbnailWithTransform: true,
            kCGImageSourceShouldCacheImmediately: true
        ]

        guard let source = CGImageSourceCreateWithData(data as CFData, nil),
              let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
            throw ImageLoadingError.processingFailed
        }

        #if canImport(UIKit)
        return UIImage(cgImage: cgImage)
        #elseif canImport(AppKit)
        return NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))
        #endif
    }
}

CachedAsyncImage.swift

import SwiftUI

/// A drop-in replacement for AsyncImage with caching support.
///
/// Uses `ImagePipeline` for memory/disk caching, request deduplication,
/// and optional image processing.
///
/// Usage:
/// ```swift
/// CachedAsyncImage(url: user.avatarURL) { image in
///     image.resizable().aspectRatio(contentMode: .fill)
/// } placeholder: {
///     ProgressView()
/// }
/// ```
struct CachedAsyncImage<Content: View, Placeholder: View>: View {
    let url: URL?
    let processing: ImageProcessingOptions?
    let content: (Image) -> Content
    let placeholder: () -> Placeholder

    @Environment(\.imagePipeline) private var pipeline
    @State private var loadedImage: PlatformImage?
    @State private var isLoading = false
    @State private var loadError: Error?

    init(
        url: URL?,
        processing: ImageProcessingOptions? = nil,
        @ViewBuilder content: @escaping (Image) -> Content,
        @ViewBuilder placeholder: @escaping () -> Placeholder
    ) {
        self.url = url
        self.processing = processing
        self.content = content
        self.placeholder = placeholder
    }

    var body: some View {
        Group {
            if let loadedImage {
                #if canImport(UIKit)
                content(Image(uiImage: loadedImage))
                #elseif canImport(AppKit)
                content(Image(nsImage: loadedImage))
                #endif
            } else {
                placeholder()
            }
        }
        .task(id: url) {
            await loadImage()
        }
    }

    private func loadImage() async {
        guard let url else { return }

        loadedImage = nil
        loadError = nil
        isLoading = true

        do {
            let image = try await pipeline.image(for: url, processing: processing)
            guard !Task.isCancelled else { return }
            loadedImage = image
        } catch {
            guard !Task.isCancelled else { return }
            loadError = error
        }

        isLoading = false
    }
}

// Convenience initializer without placeholder
extension CachedAsyncImage where Placeholder == ProgressView<EmptyView, EmptyView> {
    init(
        url: URL?,
        processing: ImageProcessingOptions? = nil,
        @ViewBuilder content: @escaping (Image) -> Content
    ) {
        self.init(url: url, processing: processing, content: content) {
            ProgressView()
        }
    }
}

ImagePrefetcher.swift

import Foundation

/// Prefetches images for upcoming list items.
///
/// Start prefetching when items are about to appear,
/// and cancel when they scroll off screen.
///
/// Usage:
/// ```swift
/// @State private var prefetcher = ImagePrefetcher()
///
/// ForEach(items) { item in
///     ItemRow(item: item)
///         .onAppear { prefetcher.startPrefetching(urls: [item.imageURL]) }
///         .onDisappear { prefetcher.stopPrefetching(urls: [item.imageURL]) }
/// }
/// ```
@Observable
final class ImagePrefetcher {
    private var activeTasks: [URL: Task<Void, Never>] = [:]
    private let pipeline: ImagePipeline

    init(pipeline: ImagePipeline = .shared) {
        self.pipeline = pipeline
    }

    /// Start prefetching images for the given URLs.
    func startPrefetching(urls: [URL]) {
        for url in urls {
            guard activeTasks[url] == nil else { continue }

            activeTasks[url] = Task {
                _ = try? await pipeline.image(for: url)
            }
        }
    }

    /// Cancel prefetching for the given URLs.
    func stopPrefetching(urls: [URL]) {
        for url in urls {
            activeTasks[url]?.cancel()
            activeTasks.removeValue(forKey: url)
        }
    }

    /// Cancel all active prefetch tasks.
    func stopAll() {
        activeTasks.values.forEach { $0.cancel() }
        activeTasks.removeAll()
    }
}

Source: SKILL.md on GitHub

No alerts2mo3 checks · Risk SAFE
  • Gen Agent Trust Hub2mo

    The skill generates a production-ready image loading and caching pipeline for Swift applications (iOS/macOS). It follows security best practices such as using SHA-256 hashing for cache filenames to prevent path traversal and employs modern Swift concurrency for thread safety. No malicious patterns, obfuscation, or unauthorized data access were detected.

  • Socket2mo

    No alerts

  • Snyk2mo

    Risk: LOW · No issues

Signed by skilld at 32566aa. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 3 months ago
What it can do
Reads files Edits files Runs commands
last_verified
2026-07-16
review_by
2027-06-22
os_version
iOS 27 / macOS 27
All 7 allowed tools
ReadWriteEditGlobGrepBashAskUserQuestion

README badge

README badge for rshankras/claude-code-apple-skills/image-loading