SwiftUI Images & Media
Images and media are crucial for creating engaging user interfaces. SwiftUI makes it easy to display images, load them asynchronously, work with system icons, and handle various image formats. In this guide, you’ll learn everything about images and media in SwiftUI.
Image Basics
Display Local Images
// Image from assets catalogImage("photo-name") .resizable() .frame(width: 200, height: 200)
// Without resizable, image displays at natural sizeImage("logo")SF Symbols (System Icons)
Image(systemName: "heart.fill") .foregroundColor(.red) .font(.largeTitle)
Image(systemName: "star.fill") .imageScale(.large) .foreground
Color(.yellow)Resizable Modifier
Make images scale to fit:
Image("photo") .resizable() .frame(width: 300, height: 200)
// Without resizable, frame has no effect on image sizeAspect Ratio
Control how images fit within frames:
// Fit - entire image visibleImage("photo") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 300, height: 200)
// Fill - fills frame, may cropImage("photo") .resizable() .aspectRatio(contentMode: .fill) .frame(width: 300, height: 200) .clipped() // Clip overflowing parts
// Specific aspect ratioImage("photo") .resizable() .aspectRatio(16/9, contentMode: .fit)Scaling Images
// Scale to fit containerImage("photo") .resizable() .scaledToFit() .frame(width: 300)
// Scale to fill containerImage("photo") .resizable() .scaledToFill() .frame(width: 300, height: 200) .clipped()Image Shapes
Clip images to shapes:
// CircleImage("profile") .resizable() .scaledToFill() .frame(width: 100, height: 100) .clipShape(Circle())
// Rounded rectangleImage("photo") .resizable() .scaledToFill() .frame(width: 200, height: 150) .clipShape(RoundedRectangle(cornerRadius: 20))
// CapsuleImage("banner") .resizable() .scaledToFill() .frame(width: 300, height: 100) .clipShape(Capsule())AsyncImage
Load images from URLs:
// Basic async loadingAsyncImage(url: URL(string: "https://example.com/image.jpg")) .frame(width: 200, height: 200)
// With scale parameterAsyncImage(url: URL(string: "https://example.com/image.jpg"), scale: 2.0)With Custom Content
AsyncImage(url: URL(string: "https://example.com/image.jpg")) { image in image .resizable() .scaledToFit()} placeholder: { ProgressView()}.frame(width: 200, height: 200)With Phase Handling
AsyncImage(url: URL(string: "https://example.com/image.jpg")) { phase in switch phase { case .empty: ProgressView() case .success(let image): image .resizable() .scaledToFit() case .failure: Image(systemName: "photo") .foregroundColor(.gray) @unknown default: EmptyView() }}.frame(width: 200, height: 200)Complete AsyncImage Example
struct AsyncImageExample: View { let imageURL = "https://picsum.photos/200/300"
var body: some View { AsyncImage(url: URL(string: imageURL)) { phase in if let image = phase.image { image .resizable() .scaledToFill() .frame(width: 200, height: 300) .clipShape(RoundedRectangle(cornerRadius: 15)) .shadow(radius: 10) } else if phase.error != nil { VStack { Image(systemName: "exclamationmark.triangle") .font(.largeTitle) Text("Failed to load") .font(.caption) } .frame(width: 200, height: 300) .background(Color.gray.opacity(0.2)) .cornerRadius(15) } else { ZStack { Color.gray.opacity(0.2) ProgressView() } .frame(width: 200, height: 300) .cornerRadius(15) } } }}Image Rendering Modes
// Original (default)Image("icon") .renderingMode(.original)
// Template (tintable)Image("icon") .renderingMode(.template) .foregroundColor(.blue)
Image(systemName: "heart.fill") .renderingMode(.template) .foregroundColor(.red)Image Interpolation
Control how images are scaled:
Image("pixel-art") .interpolation(.none) // Pixelated, no smoothing .resizable() .scaledToFit()
Image("photo") .interpolation(.high) // Smooth scaling .resizable() .scaledToFit()Image Overlay & Background
// OverlayImage("photo") .resizable() .scaledToFit() .overlay( Text("Overlay Text") .font(.headline) .foregroundColor(.white) .padding(), alignment: .bottom )
// BackgroundText("Text on Image") .font(.largeTitle) .foregroundColor(.white) .padding() .background( Image("background") .resizable() .scaledToFill() )SF Symbols Variants
// Different variantsImage(systemName: "heart") // OutlineImage(systemName: "heart.fill") // Filled
// MulticolorImage(systemName: "folder.fill.badge.plus") .symbolRenderingMode(.multicolor)
// HierarchicalImage(systemName: "cloud.sun.rain.fill") .symbolRenderingMode(.hierarchical) .foregroundColor(.blue)
// PaletteImage(systemName: "person.3.fill") .symbolRenderingMode(.palette) .foregroundStyle(.blue, .green, .red)Custom Image Processing
// GrayscaleImage("photo") .resizable() .scaledToFit() .grayscale(1.0)
// BlurImage("photo") .resizable() .scaledToFit() .blur(radius: 5)
// SaturationImage("photo") .resizable() .scaledToFit() .saturation(2.0)
// ContrastImage("photo") .resizable() .scaledToFit() .contrast(1.5)
// BrightnessImage("photo") .resizable() .scaledToFit() .brightness(0.2)Image Grid
Display multiple images in a grid:
struct ImageGrid: View { let images = ["photo1", "photo2", "photo3", "photo4"] let columns = [ GridItem(.flexible()), GridItem(.flexible()) ]
var body: some View { ScrollView { LazyVGrid(columns: columns, spacing: 10) { ForEach(images, id: \.self) { imageName in Image(imageName) .resizable() .scaledToFill() .frame(width: 150, height: 150) .clipShape(RoundedRectangle(cornerRadius: 10)) } } .padding() } }}##Practical Examples
Profile Picture
struct ProfilePicture: View { let imageURL: String let size: CGFloat = 80
var body: some View { AsyncImage(url: URL(string: imageURL)) { phase in if let image = phase.image { image .resizable() .scaledToFill() } else { Image(systemName: "person.circle.fill") .resizable() .foregroundColor(.gray) } } .frame(width: size, height: size) .clipShape(Circle()) .overlay(Circle().stroke(Color.white, lineWidth: 2)) .shadow(radius: 3) }}Image Card
struct ImageCard: View { let imageURL: String let title: String let subtitle: String
var body: some View { VStack(alignment: .leading, spacing: 0) { AsyncImage(url: URL(string: imageURL)) { image in image .resizable() .scaledToFill() } placeholder: { Color.gray.opacity(0.3) } .frame(height: 200) .clipped()
VStack(alignment: .leading, spacing: 4) { Text(title) .font(.headline) Text(subtitle) .font(.subheadline) .foregroundColor(.gray) } .padding() } .background(Color.white) .cornerRadius(12) .shadow(radius: 5) }}Banner Image
struct BannerImage: View { var body: some View { ZStack(alignment: .bottomLeading) { Image("banner") .resizable() .scaledToFill() .frame(height: 250) .clipped()
LinearGradient( colors: [.clear, .black.opacity(0.7)], startPoint: .top, endPoint: .bottom )
VStack(alignment: .leading) { Text("Featured") .font(.title) .bold() Text("Discover amazing content") .font(.subheadline) } .foregroundColor(.white) .padding() } }}Best Practices
1. Always Make Images Resizable
// ✅ Good - Resizable for flexible sizingImage("photo") .resizable() .scaledToFit()
// ❌ Avoid - Fixed at natural sizeImage("photo")2. Use Async Loading for Remote Images
// ✅ Good - Async with placeholderAsyncImage(url: URL(string: imageURL)) { phase in if let image = phase.image { image.resizable().scaledToFit() } else { ProgressView() }}
// ❌ Avoid - Blocking main thread// Loading images synchronously3. Clip Filled Images
// ✅ Good - Clipped to prevent overflowImage("photo") .resizable() .scaledToFill() .frame(width: 200, height: 200) .clipped()
// ❌ Avoid - Image overflows frameImage("photo") .resizable() .scaledToFill() .frame(width: 200, height: 200)4. Use SF Symbols for Icons
// ✅ Good - Scalable system iconsImage(systemName: "heart.fill") .font(.largeTitle)
// ❌ Avoid - Custom image for simple iconsImage("heart-icon")Summary
SwiftUI provides powerful image handling:
✅ Image - Display local images and SF Symbols
✅ AsyncImage - Load remote images asynchronously
✅ Resizable - Scale images to fit
✅ AspectRatio - Control image fitting
✅ ClipShape - Clip to custom shapes
✅ SF Symbols - 5000+ scalable system icons
Key Takeaways:
- Use
.resizable()for flexible image sizing AsyncImagefor loading remote images.scaledToFit()or.scaledToFill()for aspect ratio- Clip filled images to prevent overflow
- SF Symbols for consistent, scalable icons
- Handle loading states in AsyncImage
Next Steps: Learn about SwiftUI Gestures to make your images and views interactive! 🚀