Help us improve
Share bugs, ideas, or general feedback.
From ios-from-web-guide
MANDATORY for any AsyncImage rendering a URL from a JSON response. Invoke before writing AsyncImage(url:) anywhere in the View layer.
npx claudepluginhub j-morgan6/ios-from-web-guide --plugin ios-from-web-guideHow this skill is triggered — by the user, by Claude, or both
Slash command
/ios-from-web-guide:swiftui-async-image-with-backend-pathsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
1. **Every image URL from the backend goes through `String.asBackendURL`.** Never pass a raw string to `URL(string:)` — if the backend returns `/uploads/abc.jpg`, `URL(string:)` silently returns a URL with no host, and `AsyncImage` displays the placeholder forever with no error.
Creates p5.js generative art with seeded randomness, noise fields, and interactive parameter exploration. Use for algorithmic art, flow fields, or particle systems.
Share bugs, ideas, or general feedback.
AsyncImage with Backend PathsString.asBackendURL. Never pass a raw string to URL(string:) — if the backend returns /uploads/abc.jpg, URL(string:) silently returns a URL with no host, and AsyncImage displays the placeholder forever with no error.Extensions/String+BackendURL.swift on day 1. Before the first AsyncImage. Before the first feature. It's a 10-line extension that prevents 5 duplicate fullURL(_:) helpers from growing across the codebase.AsyncImage explicitly — .frame(maxWidth: .infinity).frame(height: X) or .aspectRatio(_:, contentMode:).frame(height: X).clipped(). Otherwise the natural image size bleeds up through the parent VStack (see swiftui-layout-pitfalls).AsyncImage(url:) { image in ... } placeholder: { Color.gray.opacity(0.15) } — so the frame is visually occupied during load.fullURL(_:) helper. Use .asBackendURL. This rule is enforced by hook H-W-4.AsyncImage(url:) call.// Backend responds:
// { "data": { "id": 123, "photo_url": "/uploads/abc.jpg" } }
// ❌ Classic silent failure
AsyncImage(url: URL(string: post.photoURL)) { $0.resizable() }
placeholder: { ProgressView() }
URL(string: "/uploads/abc.jpg") returns a URL with no scheme/host. AsyncImage dispatches a URLSession load against a hostless URL, gets an opaque failure, falls through to the placeholder, and reports nothing. The user sees a spinning ProgressView forever.
String.asBackendURL// Extensions/String+BackendURL.swift
extension String {
var asBackendURL: URL? {
if hasPrefix("http://") || hasPrefix("https://") {
return URL(string: self)
}
return URL(string: Configuration.apiBaseURL + self)
}
}
See <plugin-root>/templates/String+BackendURL.swift for the canonical version.
Now:
// ✅
AsyncImage(url: post.photoURL.asBackendURL) { image in
image.resizable().aspectRatio(contentMode: .fill)
} placeholder: {
Color.gray.opacity(0.15)
}
.frame(height: 240)
.clipped()
https://api.example.com/uploads/abc.jpg..asBackendURL returns URL? which AsyncImage accepts directly.Every iOS project that consumes a web-first backend hits this inside the first 2-3 features. If you don't create String+BackendURL.swift on day 1, you'll write 5 different fullURL(_:) helpers — one per view file — and then spend a half-day consolidating them.
Signs you've waited too long:
fullURL, imageURL, resolveURL helpers scattered across views.struct FeedCardView: View {
let post: Post
var body: some View {
VStack(alignment: .leading, spacing: 8) {
AsyncImage(url: post.coverImageURL.asBackendURL) { image in
image.resizable().aspectRatio(contentMode: .fill)
} placeholder: {
Color.gray.opacity(0.15)
}
.frame(maxWidth: .infinity)
.frame(height: 240)
.clipped()
.cornerRadius(12)
Text(post.title).font(.headline)
}
}
}
The simulator and device run identical code. The backend just happens to return different URL shapes in different environments — often signed S3 URLs in prod and /uploads/... in dev. .asBackendURL handles both.
That's the exact behavior .asBackendURL prevents. If you see this symptom anywhere, grep for URL(string: and replace with .asBackendURL.
URL(string:) in a View fileThis is the enforcement path. The hook scans files under Views/ and flags URL(string: calls that don't go through .asBackendURL. Fix by adopting the extension.
Usually a layout bug, not a URL bug — see swiftui-layout-pitfalls. But double-check the URL isn't being recomputed into nil somewhere.
An earlier, buggier version of the extension prepended the base URL unconditionally:
// ❌ Don't do this
var asBackendURL: URL? { URL(string: Configuration.apiBaseURL + self) }
This turns https://s3.amazonaws.com/foo.jpg into https://api.example.comhttps://s3.amazonaws.com/foo.jpg. Always check hasPrefix("http") first.
See <plugin-root>/templates/String+BackendURL.swift — copy into Extensions/String+BackendURL.swift on day 1.
ios-api-client-foundation — provides Configuration.apiBaseURL.swiftui-layout-pitfalls — AsyncImage frames and VStack/ScrollView interactions.ios-project-structure — where Extensions/ lives in the tree.