Swift and SwiftUI tutorials for Swift Developers

How to show an image in SwiftUI

In the world of mobile application development, visual content is king. No matter how robust your backend logic is or how efficient your sorting algorithm might be; if your user interface (UI) doesn’t appeal to the eye, user retention will drop. For an iOS Developer, the transition from UIKit to SwiftUI brought a radical simplification in how we handle visual elements.

Gone are the days of configuring UIImageView, struggling with Auto Layout constraints to maintain aspect ratio, and manually handling asynchronous resource loading. In modern Swift programming with SwiftUI, displaying an image is a declarative, intuitive, and powerful operation.

In this tutorial, we will explore every corner of the Image view in Xcode. From the basics to asynchronous cloud loading, passing through optimizations for iOS, macOS, and watchOS. Get ready to master the art of how to show an image in SwiftUI.

1. Fundamentals: The Image View in SwiftUI

At the heart of graphic visualization in SwiftUI lies the Image structure. Unlike UIImageView in UIKit, Image in SwiftUI is not a subclass of UIView; it is a lightweight view that renders a bitmap or a vector.

Adding your first image in Xcode

To start, we need a resource. In your Xcode project, you will find the Assets.xcassets file. This is the asset catalog where your images must live to ensure the App Store can apply App Thinning (downloading only the necessary resolution for the user’s device: 1x, 2x, or 3x).

  1. Open Assets.xcassets.
  2. Drag your image (let’s say, “landscape.jpg”) into the panel.
  3. Name it “landscape” for easy reference.

Now, in your SwiftUI view:

struct ContentView: View {
    var body: some View {
        Image("landscape")
    }
}

That’s it! However, if you run this, you will notice something immediately: if the image is larger than the screen, it will go out of bounds. SwiftUI renders the image at its native size by default.

2. Resizing and Aspect Ratio: Challenge #1

One of the most common mistakes when starting to show an image in SwiftUI is forgetting that images are not resizable by default. This is a design decision to optimize performance: SwiftUI assumes you want to display the exact pixel unless you tell it otherwise.

The .resizable() modifier

To make an image adapt to its container, you must explicitly apply the .resizable() modifier.

Image("landscape")
    .resizable()
    .frame(width: 300, height: 200)

By doing this, the image will stretch to fill exactly the 300×200 box, which will likely distort the image, making it look squashed or stretched.

Controlling proportions: .scaledToFit vs .scaledToFill

To maintain the original image proportions (aspect ratio), we use two key modifiers. Understanding the difference is vital for any iOS Developer.

  • .scaledToFit(): Scales the image until it fits completely inside the container. This guarantees the entire image is seen, but may leave empty spaces (letterboxing) on the sides or top/bottom if proportions don’t match.
  • .scaledToFill(): Scales the image until it fills the entire container. This eliminates empty spaces but will crop (clip) parts of the image that fall outside the visible area.

Practical Example:

Image("avatar")
    .resizable()
    .aspectRatio(contentMode: .fill) // Equivalent to .scaledToFill()
    .frame(width: 100, height: 100)
    .clipped() // Important: Cuts off what protrudes from the frame

Pro Note: If you use .scaledToFill(), you will almost always want to accompany it with .clipped(). Otherwise, the image will paint outside the bounds of its frame, overlapping with other views.

3. SF Symbols: World-Class Iconography

Apple provides a library of over 6,000 vector icons integrated into the operating system, known as SF Symbols. For a Swift programming developer, this is pure gold: icons that scale with typography, support accessibility, and adapt to font weight.

To display a system symbol, we use a different initializer:

Image(systemName: "star.fill")
    .font(.system(size: 50)) // They resize with font modifiers, not frame
    .foregroundColor(.yellow)

Hierarchical and Multicolor Rendering (iOS 15+)

Since iOS 15, SwiftUI allows advanced rendering modes for SF Symbols, enabling icons with multiple color layers.

Image(systemName: "cloud.sun.rain.fill")
    .symbolRenderingMode(.palette)
    .foregroundStyle(.gray, .yellow, .blue) // Gray cloud, yellow sun, blue rain

This elevates the visual quality of your app without needing to design complex custom assets.

4. AsyncImage: Loading Images from the Internet

In modern development, rarely are all images local. It is normal to consume a REST API that returns image URLs. Before iOS 15, you had to create your own ImageLoader with URLSession or use third-party libraries like Kingfisher or SDWebImage.

Now, SwiftUI includes AsyncImage.

Basic Usage

AsyncImage(url: URL(string: "https://example.com/image.jpg"))

This code downloads the image, shows a gray placeholder while loading, and then displays the image. But as an iOS Developer, you need more control.

Managing Loading States

For a robust user experience, we must handle the three states: Loading, Success, and Failure.

AsyncImage(url: URL(string: "https://my-server.com/profile.jpg")) { phase in
    switch phase {
    case .empty:
        ProgressView() // Native iOS spinner
            .frame(width: 100, height: 100)
            
    case .success(let image):
        image
            .resizable()
            .scaledToFill()
            .frame(width: 100, height: 100)
            .clipShape(Circle())
            
    case .failure:
        Image(systemName: "photo.circle.fill") // Error image
            .font(.system(size: 100))
            .foregroundColor(.gray)
            
    @unknown default:
        EmptyView()
    }
}

Important considerations about AsyncImage:

  1. Cache: By default, AsyncImage uses the system’s URLSession cache. However, it does not aggressively persist images to disk like dedicated libraries. If your app is an Instagram-like feed with thousands of images, consider still using optimized libraries like Kingfisher to manage memory and disk efficiently.
  2. Lists: When using AsyncImage inside a List or LazyVGrid, the download is automatically cancelled if the cell scrolls away, optimizing data usage.

5. Styling and Visual Modifiers

Once you know how to show an image in SwiftUI, the next step is making it attractive. SwiftUI offers powerful modifiers that work consistently across iOS, macOS, and watchOS.

Shapes and Clipping

The round profile photo is an industry standard.

Image("user")
    .resizable()
    .scaledToFill()
    .frame(width: 120, height: 120)
    .clipShape(Circle()) // Or RoundedRectangle(cornerRadius: 20)
    .overlay(
        Circle().stroke(Color.white, lineWidth: 4) // Border
    )
    .shadow(radius: 10) // Shadow

Filters and Color Adjustments

You don’t need an external photo editor for basic adjustments.

  • .opacity(0.5): Transparency.
  • .blur(radius: 5): Gaussian blur (ideal for backgrounds).
  • .grayscale(1.0): Converts to black and white.
  • .colorMultiply(.red): Tints the image.

6. Images as Backgrounds and Layers

Often you will want to use an image as a wallpaper or card background. The .background() modifier accepts any view, including images.

Text("Welcome to Swift")
    .font(.largeTitle)
    .padding()
    .background(
        Image("abstract_bg")
            .resizable()
            .scaledToFill()
            .edgesIgnoringSafeArea(.all) // To occupy the entire screen
    )

However, for more complex layouts, using ZStack is usually preferable for its hierarchy clarity:

ZStack {
    Image("background")
        .resizable()
        .ignoresSafeArea()
    
    VStack {
        // Content on top of the image
    }
}

7. Interoperability: UIImage and NSImage

Although we live in the future with SwiftUI, the Xcode ecosystem is vast and sometimes we need to interact with legacy code or libraries that return UIKit objects.

From UIImage to SwiftUI

If you have a UIImage object (perhaps generated by a CoreImage filter or captured by the camera):

let uiImage = UIImage(named: "legacy_asset")!
Image(uiImage: uiImage)
    .resizable()

From NSImage to SwiftUI (macOS)

If you are developing for macOS:

let nsImage = NSImage(named: "desktop")!
Image(nsImage: nsImage)
    .resizable()

This demonstrates the flexibility of Swift programming to bridge both worlds.

8. Cross-Platform Considerations: iOS, macOS, and watchOS

As a developer using SwiftUI, you have the superpower to deploy on multiple platforms. But images require special care on each one.

  • watchOS: Size matters. On Apple Watch, storage and CPU are limited.
    • Tip: Don’t use 4K images on the Watch. Use the Asset Catalog to provide specific versions for watchOS that are smaller and compressed.
    • Rendering: Avoid complex transparencies or excessive shadows in long lists (List) to maintain 60 FPS on the wrist.
  • macOS: Resizable windows. On desktop, the user can freely resize the window. Ensure your images have their resizing policy configured (.resizable()) and use GeometryReader if you need an image to occupy a specific percentage of the window width, rather than a fixed size in points.

9. Accessibility: A Duty of the iOS Developer

An image is worth a thousand words, but not for a user using VoiceOver. For your application to be professional and App Store ready, you must label your images.

There are two types of images in accessibility:

  1. Decorative Images: They provide no information (e.g., an abstract background).
    Image(decorative: "blue_wave")
    

    VoiceOver will ignore this view.

  2. Informative Images: They provide content (e.g., a product photo).
    Image("nike_sneakers")
        .resizable()
        .accessibilityLabel("Red Nike Air model sneakers")
        .accessibilityAddTraits(.isImage)
    

10. Performance Optimizations

To finish, let’s talk about performance. Loading large images consumes a lot of RAM. If your app crashes due to “Memory Warning”, check your images.

  1. Interpolation: If you are making a “Pixel Art” app and the image looks blurry when enlarged, change the interpolation.
    Image("mario_bros")
        .interpolation(.none) // Keeps pixelated edges sharp
        .resizable()
    
    • Antialiasing: If you rotate an image and the edges look jagged (.rotationEffect), make sure to enable .antialiased(true).
    • File Format:
      • Use SVG (PDF in Xcode) for simple vector icons (logos, flat icons). It allows scaling without losing quality and is lightweight.
      • Use PNG if you need transparency.
      • Use JPG or HEIC for photographs. HEIC is Apple’s native format and offers better compression than JPG with the same quality.

    Conclusion

    Knowing how to show an image in SwiftUI is just the first step. As we have seen, the Image view in Xcode is a versatile tool that goes far beyond simply painting pixels on the screen.

    From intelligent management of Assets.xcassets to asynchronous loading with AsyncImage and adapting to accessibility guidelines, mastering these concepts separates you from a beginner programmer and brings you closer to being a senior iOS Developer.

    The next time you open your Swift programming project, remember: a well-implemented image not only decorates but communicates, guides, and enhances the user experience. Experiment with modifiers, test your designs in the watchOS and iPad simulator, and create visually striking interfaces.

    If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

matchedGeometryEffect in SwiftUI

Next Article

Create a Spring Animation in SwiftUI

Related Posts