Load network images using AsyncImage in SwiftUI

  • Aug 1, 2024

Load network images using AsyncImage in SwiftUI

  • DevTechie Inc

Load network images using AsyncImage in SwiftUI

WWDC21 introduced a new view called AsyncImage for SwiftUI. Like the name suggests this view is responsible for loading images asynchronously and displaying the image once it’s finished loading.

AsyncImage can load images from local resources or from external resources but mostly you are gonna find yourself using this view to load network images coming from an API endpoint.

Let’s first take a look at its initializer:

public init(url: URL?, scale: CGFloat = 1) where Content == Image

This init expects two parameters.

  • url: represents a url that points to the image

  • scale: defines the scale of source image. This parameter has a default value of 1 but you can specify the value of your choice based on the device’s scale.

Let’s create an example for this init. For our examples, we will use a free API endpoint from pexels to fetch an image over the network.

Start by creating a new file from SwiftUI template. We will call this file AsyncImageExample and it will have our new AsyncImage view as shown below

struct AsyncImageExample: View {
    var body: some View {
            AsyncImage(url: URL(string: "https://images.pexels.com/photos/2014422/pexels-photo-2014422.jpeg?auto=compress&cs=tinysrgb&h=350"))
        }
}

While the image is being loaded, SwiftUI displays a gray color placeholder. This placeholder is a view that takes up as much space as it can, as offered by its parent view. The reason for this behavior is that the image’s size is not known until it is loaded. Once the image is loaded, its size is known, and the view adjusts its size accordingly.

Note that our placeholder image can be big and can taking the whole screen. We can resolve that by adding a frame modifier to the AsyncImage view.

struct AsyncImageExample: View {
  var body: some View {
      AsyncImage(url: URL(string: "https://images.pexels.com/photos/2014422/pexels-photo-2014422.jpeg?auto=compress&cs=tinysrgb&h=350"))
      .frame(maxWidth: 350, maxHeight: 350)
    }
}

Setting the frame would only resize the container not the image. If our image is too big then it will be drawn outside of bounds.

To change the size of our image, we will have to use another initializer of AsyncImage which will give us the image in completion so we can adjust its size, aspectRatio etc. We can also define our own placeholder view in this overload.

Signature to this init looks like following:

public init<I, P>(
url: URL?, 
scale: CGFloat = 1, 
@ViewBuilder content: @escaping (Image) -> I, 
@ViewBuilder placeholder: @escaping () -> P) 
where Content == _ConditionalContent<I, P>, I : View, P : View

We have two new parameters here.

  • content is a ViewBuilder closure that provides the Image view when image is loaded.

  • placeholder is a ViewBuilder closure that provides the view to show while image is being loaded.

struct AsyncImageExample: View {
  var body: some View {
    AsyncImage(url: URL(string: "https://images.pexels.com/photos/2014422/pexels-photo-2014422.jpeg?auto=compress&cs=tinysrgb&h=350")) { image in
      image.resizable()
        .aspectRatio(contentMode: .fit)
        .frame(maxWidth: 200, maxHeight: 200)
    } placeholder: {
      ProgressView()
    }
  }
}

This overload for AsyncImage also gives us the ability to provide a custom view for the placeholder. In this case, we are showing a ProgressView instead of the default gray rectangle view while waiting for the image to be downloaded.

AsyncImagePhase

If we need more control over various loading phases for image then we can use following init

public init(url: URL?, scale: CGFloat = 1, transaction: Transaction = Transaction(), @ViewBuilder content: @escaping (AsyncImagePhase) -> Content)

This initializer with AsyncImagePhase gives us more control over the view and lets us take action when image is yet to load, loaded or failed to load.

It takes following parameters:

  • url: represents a url that points to the image

  • scale: defines the scale of source image. This parameter has a default value of 1 but you can specify the value of your choice based on the device’s scale.

  • transaction is used to track the phase changes.

  • content is the ViewBuilder closure which provides a view for each phase of loading process. This closure receives AsyncImagePhase value.

AsyncImagePhase is an enum which has following cases:

  • empty Image is not loaded yet

  • success(Image) Image is successfully loaded and provided as parameter

  • failure(Error) Image failed to load and error is provided as parameter

We will modify our AsyncImage view a bit to move url into a variable and at the same time we will also create transaction variable.

struct AsyncImageExample: View {
   
  let url = URL(string: "https://images.pexels.com/photos/2014422/pexels-photo-2014422.jpeg?auto=compress&cs=tinysrgb&h=350")
  let transaction = Transaction(animation: Animation.easeIn(duration: 5.0))
   
  var body: some View {
    AsyncImage(url: url, scale: 1.0, transaction: transaction) { imagePhase in
       
      switch imagePhase {
      case .empty:
        ProgressView()
         
      case .success(let image):
        image
          .resizable()
          .aspectRatio(contentMode: .fit)
          .frame(maxWidth: 200, maxHeight: 200)
         
      case .failure(let err):
        VStack {
          Text("Failed to load image.")
          Text(err.localizedDescription)
        }
         
      @unknown default:
        EmptyView()
      }
       
    }
  }
}

Notice switch statement with imagePhase , which gives us access to various stages of download process.

Let’s add little delay to the url so we can appreciate progressView a bit more😍.

To emulate delay on a certain API response, we can use https://app.requestly.io/delay/ service.

https://app.requestly.io/delay/<delay in milliseconds>/<original url>

Let’s change our image url to following:

https://app.requestly.io/delay/2000/https://source.unsplash.com/random/200x200

Changed code will look like this:

struct AsyncImageExample: View {
   
  let url = URL(string: "https://app.requestly.io/delay/2000/https://images.pexels.com/photos/2014422/pexels-photo-2014422.jpeg?auto=compress&cs=tinysrgb&h=350")
  let transaction = Transaction(animation: Animation.easeIn(duration: 5.0))
   
  var body: some View {
    AsyncImage(url: url, scale: 1.0, transaction: transaction) { imagePhase in
       
      switch imagePhase {
      case .empty:
        ProgressView()
         
      case .success(let image):
        image
          .resizable()
          .aspectRatio(contentMode: .fit)
          .frame(maxWidth: 200, maxHeight: 200)
         
      case .failure(let err):
        VStack {
          Text("Failed to load image.")
          Text(err.localizedDescription)
        }
         
      @unknown default:
        EmptyView()
      }
       
    }
  }
}

Let’s explore error scenario. To emulate error case we will introduce additional letters in the image url which will make API endpoint unreachable.

let url = URL(string: "https://images.pexels_invalid.com/photos/2014422/pexels-photo-2014422.jpeg?auto=compress&cs=tinysrgb&h=450")

Our complete code will look like this

struct AsyncImageExample: View {
   
  let url = URL(string: "https://images.pexels_invalid.com/photos/2014422/pexels-photo-2014422.jpeg?auto=compress&cs=tinysrgb&h=450")
  let transaction = Transaction(animation: Animation.easeIn(duration: 5.0))
   
  var body: some View {
    AsyncImage(url: url, scale: 1.0, transaction: transaction) { imagePhase in
       
      switch imagePhase {
      case .empty:
        ProgressView()
         
      case .success(let image):
        image
          .resizable()
          .aspectRatio(contentMode: .fit)
          .frame(maxWidth: 200, maxHeight: 200)
         
      case .failure(let err):
        VStack {
          Text("Failed to load image.")
          Text(err.localizedDescription)
        }
         
      @unknown default:
        EmptyView()
      }
       
    }
  }
}

This should trigger our error workflow.

AsyncImage in List

Now that we know enough about AsyncImage. We will use it inside a list and for that we will create a dummy model so let’s create one.

struct SampleModel: Identifiable {
    let id = UUID()
    let url = URL(string: "https://app.requestly.io/delay/\(Int.random(in: 200...5000))/https://images.pexels.com/photos/2014422/pexels-photo-2014422.jpeg?auto=compress&cs=tinysrgb&h=350")
    let name = "Image \(Int.random(in: 100...5000))"
    
    static var data: [SampleModel] {
        var data = [SampleModel]()
        for _ in 0...10 {
            data.append(SampleModel())
        }
        return data
    }
}

As the code above shows, we will have a SampleModel which will conform to Identifiable protocol and will have id, urland name properties.

Url is the url we have used so far but we will randomize the delay so images load at different time not all together.

Let’s put together our SwiftUI view.

struct AsyncImageExample: View {
    
    let data = SampleModel.data
    
    var body: some View {
        List {
            ForEach(data) { item in
                LazyHStack {
                    getAsyncImage(for: item.url)
                    Text(item.name)
                }
            }
        }
    }
    
    private func getAsyncImage(for url: URL?) -> some View {
        AsyncImage(url: url, scale: 1.0, transaction: Transaction(animation: Animation.easeIn(duration: 2))) { imagePhase in
            
            switch imagePhase {
            case .empty:
                ProgressView()
                
            case .success(let image):
                image
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(maxWidth: 100, maxHeight: 100)
                
            case .failure(let err):
                VStack {
                    Text("Failed to load image.")
                    Text(err.localizedDescription)
                }
                
            @unknown default:
                EmptyView()
            }
            
        }
    }
}

In this view, we have data variable to store our sample data and then we create a List inside the list, we have ForEach and LazyHStack. We are also creating a helper function which will help us create AsyncImage for a given url.