Skip to content

Instantly share code, notes, and snippets.

@jacobsapps
Created January 14, 2026 14:00
Show Gist options
  • Select an option

  • Save jacobsapps/df056d70040d46608d793c2d3184028f to your computer and use it in GitHub Desktop.

Select an option

Save jacobsapps/df056d70040d46608d793c2d3184028f to your computer and use it in GitHub Desktop.
import SwiftUI
import UIKit
final class ImageCache {
static let shared = ImageCache()
private let cache = NSCache<NSURL, UIImage>()
private init() {}
func image(for url: URL) -> UIImage? {
cache.object(forKey: url as NSURL)
}
func insert(_ image: UIImage, for url: URL) {
cache.setObject(image, forKey: url as NSURL)
}
}
struct CachedAsyncImage: View {
enum Phase {
case empty
case loading
case success(UIImage)
case failure
}
let url: URL?
let contentMode: ContentMode
let cornerRadius: CGFloat
let palette: DrillPalette
@State private var phase: Phase = .empty
@State private var loadTask: Task<Void, Never>?
init(url: URL?, contentMode: ContentMode = .fill, cornerRadius: CGFloat = 18, palette: DrillPalette) {
self.url = url
self.contentMode = contentMode
self.cornerRadius = cornerRadius
self.palette = palette
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.fill(palette.card)
content
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
}
.overlay(
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.stroke(palette.outline, lineWidth: 1)
)
.onAppear {
loadIfNeeded()
}
.onChange(of: url) { _ in
loadIfNeeded()
}
.onDisappear {
cancelLoad()
}
}
@ViewBuilder
private var content: some View {
switch phase {
case .empty, .loading:
VStack(spacing: 8) {
ProgressView()
.tint(palette.accent)
Text("Loading")
.font(DrillTheme.bodyFont)
.foregroundStyle(palette.secondaryText)
}
case .success(let image):
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: contentMode)
case .failure:
VStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 24))
.foregroundStyle(palette.accent)
Text("Failed to load")
.font(DrillTheme.bodyFont)
.foregroundStyle(palette.secondaryText)
}
}
}
private func loadIfNeeded() {
guard let url else {
cancelLoad()
phase = .empty
return
}
if let cached = ImageCache.shared.image(for: url) {
phase = .success(cached)
return
}
cancelLoad()
phase = .loading
loadTask = Task {
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard !Task.isCancelled else { return }
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
guard let image = UIImage(data: data) else {
throw URLError(.cannotDecodeContentData)
}
ImageCache.shared.insert(image, for: url)
await MainActor.run {
phase = .success(image)
}
} catch {
guard !Task.isCancelled else { return }
await MainActor.run {
phase = .failure
}
}
}
}
private func cancelLoad() {
loadTask?.cancel()
loadTask = nil
}
}
struct AsyncImageComponentView: View {
private let palette = DrillTheme.component
private let samples: [ImageSample] = [
ImageSample(title: "Cliffside", url: URL(string: "https://picsum.photos/id/1011/600/600")),
ImageSample(title: "Studio", url: URL(string: "https://picsum.photos/id/1005/600/600")),
ImageSample(title: "Landscape", url: URL(string: "https://picsum.photos/id/1002/600/600")),
ImageSample(title: "Market", url: URL(string: "https://picsum.photos/id/1035/600/600")),
ImageSample(title: "Rainy", url: URL(string: "https://picsum.photos/id/1027/600/600")),
ImageSample(title: "Amber", url: URL(string: "https://picsum.photos/id/1024/600/600"))
]
private var columns: [GridItem] {
[GridItem(.flexible(), spacing: 14), GridItem(.flexible(), spacing: 14)]
}
var body: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
SectionHeader(
"Async Image Component",
subtitle: "Build your own image loader with caching, cancellation, and error states.",
palette: palette
)
DrillCard(
title: "Component Goals",
bullets: [
"No AsyncImage - manage URLSession and state yourself.",
"Cache successful images to avoid repeat work.",
"Cancel in-flight requests when views disappear.",
"Surface loading and error states in the UI."
],
palette: palette
)
LazyVGrid(columns: columns, spacing: 14) {
ForEach(samples) { sample in
VStack(alignment: .leading, spacing: 8) {
CachedAsyncImage(
url: sample.url,
contentMode: .fill,
cornerRadius: 18,
palette: palette
)
.frame(height: 150)
Text(sample.title)
.font(DrillTheme.bodyStrongFont)
.foregroundStyle(palette.title)
}
}
}
}
.padding(24)
}
.navigationTitle("Component")
.navigationBarTitleDisplayMode(.inline)
}
.drillBackground(palette)
}
}
struct ImageSample: Identifiable {
let id = UUID()
let title: String
let url: URL?
}
#Preview {
AsyncImageComponentView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment