Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
80
100%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Advisory
Suggest reviewing before use
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.3, backward-compatible to iOS 16 unless noted.
AsyncImage (iOS 15+) provides built-in async image loading from a URL. Suitable for simple cases but has significant limitations for production use.
AsyncImage(url: URL(string: "https://example.com/photo.jpg"))AsyncImage(url: imageURL) { phase in
switch phase {
case .empty:
ProgressView()
.frame(width: 200, height: 200)
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 200, height: 200)
.clipped()
case .failure:
Image(systemName: "photo")
.font(.largeTitle)
.foregroundStyle(.secondary)
.frame(width: 200, height: 200)
@unknown default:
EmptyView()
}
}AsyncImage(url: imageURL, transaction: Transaction(animation: .easeIn(duration: 0.3))) { phase in
switch phase {
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.transition(.opacity)
default:
Color.secondary.opacity(0.2)
}
}
.frame(width: 200, height: 200)
.clipShape(.rect(cornerRadius: 12))AsyncImage has several shortcomings for production apps:
For anything beyond simple, low-volume image display, use a custom loading pipeline.
An actor-isolated in-memory image cache backed by NSCache. Thread-safe and automatically evicts entries under memory pressure.
import UIKit
actor ImageCache {
static let shared = ImageCache()
private let cache = NSCache<NSString, UIImage>()
private var inFlightTasks: [URL: Task<UIImage?, Never>] = [:]
init(countLimit: Int = 100, totalCostLimit: Int = 50 * 1024 * 1024) {
cache.countLimit = countLimit
cache.totalCostLimit = totalCostLimit // 50 MB default
}
func image(for url: URL) -> UIImage? {
cache.object(forKey: url.absoluteString as NSString)
}
func store(_ image: UIImage, for url: URL) {
let cost = image.jpegData(compressionQuality: 1.0)?.count ?? 0
cache.setObject(image, forKey: url.absoluteString as NSString, cost: cost)
}
func removeImage(for url: URL) {
cache.removeObject(forKey: url.absoluteString as NSString)
}
func removeAll() {
cache.removeAllObjects()
inFlightTasks.removeAll()
}
/// Fetch an image with request coalescing. Multiple callers for the same URL
/// share a single network request.
func fetch(from url: URL, session: URLSession = .shared) async -> UIImage? {
// Return cached image immediately
if let cached = image(for: url) {
return cached
}
// Coalesce duplicate in-flight requests
if let existingTask = inFlightTasks[url] {
return await existingTask.value
}
let task = Task<UIImage?, Never> {
defer { inFlightTasks[url] = nil }
guard let (data, response) = try? await session.data(from: url),
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let image = UIImage(data: data) else {
return nil
}
store(image, for: url)
return image
}
inFlightTasks[url] = task
return await task.value
}
}struct CachedImageView: View {
let url: URL
@State private var image: UIImage?
var body: some View {
Group {
if let image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
Color.secondary.opacity(0.2)
.overlay(ProgressView())
}
}
.task(id: url) {
image = await ImageCache.shared.fetch(from: url)
}
}
}Configure URLCache for persistent image caching that survives app restarts. URLCache handles HTTP cache headers automatically.
enum ImageSessionConfiguration {
static func makeSession() -> URLSession {
let config = URLSessionConfiguration.default
// 50 MB memory / 200 MB disk
config.urlCache = URLCache(
memoryCapacity: 50 * 1024 * 1024,
diskCapacity: 200 * 1024 * 1024,
directory: cacheDirectory
)
config.requestCachePolicy = .returnCacheDataElseLoad
config.httpMaximumConnectionsPerHost = 6
return URLSession(configuration: config)
}
private static var cacheDirectory: URL {
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
.appendingPathComponent("ImageCache", isDirectory: true)
}
}func cachedImageData(from url: URL, session: URLSession) async throws -> Data {
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
let (data, _) = try await session.data(for: request)
return data
}func refreshImageData(from url: URL, session: URLSession) async throws -> Data {
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
let (data, _) = try await session.data(for: request)
return data
}func cleanImageCache(session: URLSession) {
session.configuration.urlCache?.removeAllCachedResponses()
}
func removeCachedImage(for url: URL, session: URLSession) {
let request = URLRequest(url: url)
session.configuration.urlCache?.removeCachedResponse(for: request)
}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.
import ImageIO
import UIKit
func downsample(data: Data, to pointSize: CGSize, scale: CGFloat = UITraitCollection.current.displayScale) -> UIImage? {
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
]
guard let source = CGImageSourceCreateWithData(data as CFData, nil),
let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
return nil
}
return UIImage(cgImage: cgImage)
}func downsample(url: URL, to pointSize: CGSize, scale: CGFloat = UITraitCollection.current.displayScale) -> UIImage? {
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
]
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
return nil
}
return UIImage(cgImage: cgImage)
}| Source Size | Display Size | Without Downsampling | With Downsampling |
|---|---|---|---|
| 4032x3024 | 100x100 pt @3x | ~46 MB | ~0.35 MB |
| 4032x3024 | 300x300 pt @3x | ~46 MB | ~3.1 MB |
| 1920x1080 | 100x100 pt @3x | ~7.9 MB | ~0.35 MB |
Prefetch images before they scroll into view. Works with both UICollectionView data source prefetching and SwiftUI List.
@Observable
@MainActor
final class ImagePrefetcher {
private let cache: ImageCache
private let session: URLSession
private var prefetchTasks: [URL: Task<Void, Never>] = [:]
init(cache: ImageCache = .shared,
session: URLSession = ImageSessionConfiguration.makeSession()) {
self.cache = cache
self.session = session
}
func prefetch(urls: [URL]) {
for url in urls {
guard prefetchTasks[url] == nil else { continue }
prefetchTasks[url] = Task {
_ = await cache.fetch(from: url, session: session)
prefetchTasks[url] = nil
}
}
}
func cancelPrefetch(urls: [URL]) {
for url in urls {
prefetchTasks[url]?.cancel()
prefetchTasks[url] = nil
}
}
func cancelAll() {
prefetchTasks.values.forEach { $0.cancel() }
prefetchTasks.removeAll()
}
}struct PhotoGrid: View {
let photos: [Photo]
@State private var prefetcher = ImagePrefetcher()
private let columns = [GridItem(.adaptive(minimum: 100), spacing: 2)]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 2) {
ForEach(photos) { photo in
CachedImageView(url: photo.thumbnailURL)
.aspectRatio(1, contentMode: .fill)
.clipped()
.onAppear {
prefetchNearby(photo)
}
}
}
}
.onDisappear {
prefetcher.cancelAll()
}
}
private func prefetchNearby(_ photo: Photo) {
guard let index = photos.firstIndex(where: { $0.id == photo.id }) else { return }
let prefetchRange = (index + 1)..<min(index + 10, photos.count)
let urls = prefetchRange.map { photos[$0].thumbnailURL }
prefetcher.prefetch(urls: urls)
}
}final class PhotoCollectionPrefetcher: NSObject, UICollectionViewDataSourcePrefetching {
private let prefetcher = ImagePrefetcher()
private let photos: [Photo]
init(photos: [Photo]) {
self.photos = photos
}
func collectionView(_ collectionView: UICollectionView,
prefetchItemsAt indexPaths: [IndexPath]) {
let urls = indexPaths.map { photos[$0.item].thumbnailURL }
prefetcher.prefetch(urls: urls)
}
func collectionView(_ collectionView: UICollectionView,
cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
let urls = indexPaths.map { photos[$0.item].thumbnailURL }
prefetcher.cancelPrefetch(urls: urls)
}
}HEIF (High Efficiency Image Format) is the default camera format on modern iPhones. Handle detection, display, and conversion.
import UniformTypeIdentifiers
func isHEIF(data: Data) -> Bool {
guard data.count >= 12 else { return false }
// Check for 'ftyp' box at byte 4
let ftypRange = data[4..<8]
return ftypRange.elementsEqual("ftyp".utf8)
}
func isHEIF(url: URL) -> Bool {
guard let type = UTType(filenameExtension: url.pathExtension) else { return false }
return type.conforms(to: .heif) || type.conforms(to: .heic)
}func convertHEICToJPEG(data: Data, compressionQuality: CGFloat = 0.9) -> Data? {
guard let image = UIImage(data: data) else { return nil }
return image.jpegData(compressionQuality: compressionQuality)
}func convertHEICToPNG(data: Data) -> Data? {
guard let image = UIImage(data: data) else { return nil }
return image.pngData()
}import ImageIO
func convertHEICToJPEG(sourceData: Data,
quality: CGFloat = 0.9,
preserveMetadata: Bool = true) -> Data? {
guard let source = CGImageSourceCreateWithData(sourceData as CFData, nil),
let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else {
return nil
}
let mutableData = NSMutableData()
guard let destination = CGImageDestinationCreateWithData(
mutableData, UTType.jpeg.identifier as CFString, 1, nil
) else {
return nil
}
var options: [CFString: Any] = [
kCGImageDestinationLossyCompressionQuality: quality
]
// Preserve EXIF, GPS, and other metadata
if preserveMetadata,
let metadata = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) {
options[kCGImageDestinationMergeMetadata] = true
CGImageDestinationAddImage(destination, cgImage, metadata)
} else {
CGImageDestinationAddImage(destination, cgImage, options as CFDictionary)
}
guard CGImageDestinationFinalize(destination) else { return nil }
return mutableData as Data
}Reduce file size before uploading to a server. Balance quality and size based on the use case.
func compressForUpload(image: UIImage,
maxBytes: Int = 1_000_000,
initialQuality: CGFloat = 0.9) -> Data? {
var quality = initialQuality
while quality > 0.1 {
guard let data = image.jpegData(compressionQuality: quality) else { return nil }
if data.count <= maxBytes {
return data
}
quality -= 0.1
}
// Final attempt at minimum quality
return image.jpegData(compressionQuality: 0.1)
}func resizeAndCompress(image: UIImage,
maxDimension: CGFloat = 1920,
compressionQuality: CGFloat = 0.8) -> Data? {
let size = image.size
let scale: CGFloat
if max(size.width, size.height) > maxDimension {
scale = maxDimension / max(size.width, size.height)
} else {
scale = 1.0
}
let newSize = CGSize(width: size.width * scale, height: size.height * scale)
let renderer = UIGraphicsImageRenderer(size: newSize)
let resized = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: newSize))
}
return resized.jpegData(compressionQuality: compressionQuality)
}func compressAsHEIF(image: UIImage, quality: CGFloat = 0.8) -> Data? {
guard let cgImage = image.cgImage else { return nil }
let mutableData = NSMutableData()
guard let destination = CGImageDestinationCreateWithData(
mutableData, UTType.heic.identifier as CFString, 1, nil
) else {
return nil
}
let options: [CFString: Any] = [
kCGImageDestinationLossyCompressionQuality: quality
]
CGImageDestinationAddImage(destination, cgImage, options as CFDictionary)
guard CGImageDestinationFinalize(destination) else { return nil }
return mutableData as Data
}Monitor and respond to memory pressure to keep your image pipeline stable.
@Observable
@MainActor
final class MemoryMonitor {
var isUnderPressure = false
init() {
NotificationCenter.default.addObserver(
forName: UIApplication.didReceiveMemoryWarningNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.handleMemoryWarning()
}
}
private func handleMemoryWarning() {
isUnderPressure = true
Task {
await ImageCache.shared.removeAll()
}
// Reset after a delay
Task {
try? await Task.sleep(for: .seconds(10))
isUnderPressure = false
}
}
}func currentMemoryUsageMB() -> Double {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
let result = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
guard result == KERN_SUCCESS else { return 0 }
return Double(info.resident_size) / (1024 * 1024)
}actor AdaptiveImageCache {
private let cache = NSCache<NSString, UIImage>()
init() {
let totalMemory = ProcessInfo.processInfo.physicalMemory
// Use at most 10% of physical memory for image cache
let budgetBytes = Int(totalMemory / 10)
cache.totalCostLimit = budgetBytes
cache.countLimit = 200
}
func image(for url: URL) -> UIImage? {
cache.object(forKey: url.absoluteString as NSString)
}
func store(_ image: UIImage, for url: URL) {
let cost = cgImageMemorySize(image)
cache.setObject(image, forKey: url.absoluteString as NSString, cost: cost)
}
/// Estimate the decoded bitmap size in bytes.
private func cgImageMemorySize(_ image: UIImage) -> Int {
guard let cgImage = image.cgImage else { return 0 }
return cgImage.bytesPerRow * cgImage.height
}
func purge() {
cache.removeAllObjects()
}
}A production-ready pipeline that combines in-memory caching, disk caching via URLCache, downsampling, and request coalescing.
import UIKit
import ImageIO
actor ImageLoader {
static let shared = ImageLoader()
private let memoryCache = NSCache<NSString, UIImage>()
private let session: URLSession
private var inFlightTasks: [URL: Task<UIImage?, Never>] = [:]
init() {
let config = URLSessionConfiguration.default
config.urlCache = URLCache(
memoryCapacity: 50 * 1024 * 1024,
diskCapacity: 200 * 1024 * 1024
)
config.requestCachePolicy = .returnCacheDataElseLoad
config.httpMaximumConnectionsPerHost = 6
session = URLSession(configuration: config)
let totalMemory = ProcessInfo.processInfo.physicalMemory
memoryCache.totalCostLimit = Int(totalMemory / 10)
memoryCache.countLimit = 200
}
/// Load an image, optionally downsampling to the given display size.
func load(from url: URL, displaySize: CGSize? = nil) async -> UIImage? {
let cacheKey = cacheKey(url: url, size: displaySize)
// 1. Check memory cache
if let cached = memoryCache.object(forKey: cacheKey as NSString) {
return cached
}
// 2. Coalesce duplicate in-flight requests
if let existing = inFlightTasks[url] {
return await existing.value
}
let task = Task<UIImage?, Never> {
defer { inFlightTasks[url] = nil }
guard let (data, response) = try? await session.data(from: url),
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
return nil
}
// 3. Downsample if a display size is provided
let image: UIImage?
if let displaySize {
image = Self.downsample(data: data, to: displaySize)
} else {
image = UIImage(data: data)
}
// 4. Store in memory cache
if let image {
let cost = Self.bitmapSize(of: image)
memoryCache.setObject(image, forKey: cacheKey as NSString, cost: cost)
}
return image
}
inFlightTasks[url] = task
return await task.value
}
/// Remove a specific URL from the memory cache.
func evict(url: URL, displaySize: CGSize? = nil) {
let key = cacheKey(url: url, size: displaySize)
memoryCache.removeObject(forKey: key as NSString)
}
/// Purge all in-memory cached images.
func purgeMemoryCache() {
memoryCache.removeAllObjects()
inFlightTasks.values.forEach { $0.cancel() }
inFlightTasks.removeAll()
}
// MARK: - Private
private func cacheKey(url: URL, size: CGSize?) -> String {
if let size {
return "\(url.absoluteString)_\(Int(size.width))x\(Int(size.height))"
}
return url.absoluteString
}
private static func downsample(data: Data, to pointSize: CGSize) -> UIImage? {
let scale = UITraitCollection.current.displayScale
let maxDimension = max(pointSize.width, pointSize.height) * scale
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimension
]
guard let source = CGImageSourceCreateWithData(data as CFData, nil),
let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
return nil
}
return UIImage(cgImage: cgImage)
}
private static func bitmapSize(of image: UIImage) -> Int {
guard let cgImage = image.cgImage else { return 0 }
return cgImage.bytesPerRow * cgImage.height
}
}struct PipelineImageView: View {
let url: URL
var displaySize: CGSize = CGSize(width: 300, height: 300)
@State private var image: UIImage?
@State private var isLoading = true
var body: some View {
ZStack {
if let image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
} else if isLoading {
Color.secondary.opacity(0.15)
.overlay(ProgressView())
} else {
Color.secondary.opacity(0.15)
.overlay {
Image(systemName: "photo")
.font(.title2)
.foregroundStyle(.secondary)
}
}
}
.frame(width: displaySize.width, height: displaySize.height)
.clipped()
.task(id: url) {
isLoading = true
image = await ImageLoader.shared.load(from: url, displaySize: displaySize)
isLoading = false
}
}
}struct PhotoGridView: View {
let photos: [Photo]
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100), spacing: 2)], spacing: 2) {
ForEach(photos) { photo in
PipelineImageView(
url: photo.thumbnailURL,
displaySize: CGSize(width: 100, height: 100)
)
}
}
}
.onReceive(NotificationCenter.default.publisher(
for: UIApplication.didReceiveMemoryWarningNotification
)) { _ in
Task {
await ImageLoader.shared.purgeMemoryCache()
}
}
}
}| Scenario | Recommended Approach |
|---|---|
| Simple profile avatar | AsyncImage |
| Photo grid with scrolling | ImageLoader + downsampling + prefetch |
| Offline-capable gallery | ImageLoader + URLCache disk caching |
| Chat message images | ImageLoader + in-memory cache |
| Full-resolution photo viewer | ImageLoader without downsampling |
| Thumbnail in a widget | Downsample at fetch time, store in app group |
.tessl-plugin
skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-ettrace-performance
ios-localization
ios-memgraph-analysis
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
references
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
references
swift-charts
references
swift-codable
references
swift-concurrency
swift-formatstyle
references
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references