Skip to Content
Course content

213: Building a Recipe App with Async Image Loading

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing code for a junior developer who had built a beautiful recipe app. The UI was sleek, the typography was spot on, but the second I started scrolling through the "Desserts" category, the whole app began to stutter. It felt like the interface was fighting me. He had written a custom image loader that fetched data synchronously on the main thread—essentially telling the app, "Stop everything you're doing, including updating the screen, until this 2MB photo of a chocolate cake finishes downloading." It's a classic mistake, but it's a painful one for the user.

In a modern Swift app, especially one driven by a feed of images, you can't let the network hang your UI. We need to load images asynchronously. While there are many third-party libraries for this, SwiftUI's AsyncImage is usually the best place to start. It handles the heavy lifting of fetching the data in the background and updating the view once the image arrives, all without blocking the main thread.

Taming the Image Flicker with AsyncImage

The simplest way to use AsyncImage is to just pass it a URL. But if you do that in a production recipe app, your users will see a jarring "pop" where the image suddenly appears out of nowhere. I've found that this makes an app feel unpolished. To fix this, you should use the initializer that gives you access to the loading state.

struct RecipeRow: View {
    let recipe: Recipe

    var body: some View {
        HStack {
            AsyncImage(url: recipe.imageUrl) { phase in
                if let image = phase.image {
                    image
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                } else if phase.error != nil {
                    // I always suggest a specific "fallback" image here
                    Image(systemName: "fork.knife")
                        .foregroundColor(.gray)
                } else {
                    // This is the "loading" state
                    ProgressView()
                }
            }
            .frame(width: 60, height: 60)
            .cornerRadius(8)

            Text(recipe.name)
                .font(.headline)
        }
    }
}

Notice how we use the phase closure. By checking if we have an image, an error, or are still waiting, we can provide a seamless transition. I personally prefer a ProgressView over a blank space because it tells the user, "I'm working on it," which reduces the perceived wait time.

Managing Image Scaling and Memory

One thing AsyncImage doesn't do automatically is resize the actual image data coming off the wire; it only resizes the view. If your recipe API is sending back 4K images of lasagna, you're going to eat up the device's RAM very quickly. While AsyncImage is great for simple use cases, be mindful of your source images.

When you're building your recipe list, always ensure you're applying .resizable() and .aspectRatio(contentMode: .fill) inside the phase where the image is successfully loaded. If you try to apply these modifiers to the AsyncImage container itself, you'll find they simply don't work. This is a quirk of how SwiftUI wraps the underlying image view—it's a common point of frustration, so just remember: modifiers go on the image, not the AsyncImage.

Handling Failed Downloads Gracefully

Network requests fail. It's a fact of life. Maybe the user is in a subway tunnel, or the recipe image URL is broken. If you don't handle the phase.error state, your app will just show a blank gap, which looks like a bug. By providing a system icon—like fork.knife or photo—you maintain the visual rhythm of your list even when the data is missing. It’s a small detail, but it’s what separates a "student project" from a professional piece of software.




📋 Practical Task

Exercise: Implementing a Recipe Gallery Grid with Loading States

Build a RecipeGalleryView that displays a grid of recipe images. Your goal is to implement a robust loading experience that prevents the UI from feeling "empty" while images are fetching.

  • Create a Recipe model with a name and an imageUrl.
  • Use a LazyVGrid to display these recipes in two columns.
  • Implement AsyncImage using the phase closure for each recipe.
  • Requirement 1: While the image is loading, display a ZStack containing a light gray rounded rectangle and a ProgressView centered inside it.
  • Requirement 2: If the image fails to load, display a custom Image(systemName: "exclamationmark.triangle") with a caption saying "Image unavailable".
  • Requirement 3: Ensure the loaded images are clipped to a corner radius of 12 and fill their designated frame without distorting the aspect ratio.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.