UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Loading an image from a remote server

Paul Hudson    @twostraws   

SwiftUI’s Image view works great with images in your app bundle, but if you want to load a remote image from the internet you need to use AsyncImage instead. These are created using an image URL rather than a simple asset name or Xcode-generated constant, but SwiftUI takes care of all the rest for us – it downloads the image, caches the download, and displays it automatically.

So, the simplest image we can create looks like this:

AsyncImage(url: URL(string: "https://hws.dev/img/logo.png"))

I created that picture to be 1200 pixels high, but when it displays you’ll see it’s much bigger. This gets straight to one of the fundamental complexities of using AsyncImage: SwiftUI knows nothing about the image until our code is run and the image is downloaded, and so it isn’t able to size it appropriately ahead of time.

If I were to include that 1200px image in my project, I’d actually name it logo@3x.png, then also add an 800px image that was logo@2x.png. SwiftUI would then take care of loading the correct image for us, and making sure it appeared nice and sharp, and at the correct size too. As it is, SwiftUI loads that image as if it were designed to be shown at 1200 pixels high – it will be much bigger than our screen, and will look a bit blurry too.

To fix this, we can tell SwiftUI ahead of time that we’re trying to load a 3x scale image, like this:

AsyncImage(url: URL(string: "https://hws.dev/img/logo.png"), scale: 3)

When you run the code now you’ll see the resulting image is a much more reasonable size.

And if you wanted to give it a precise size? Well, then you might start by trying this:

AsyncImage(url: URL(string: "https://hws.dev/img/logo.png"))
    .frame(width: 200, height: 200)

That won’t work, but perhaps that won’t even surprise you because it wouldn’t work with a regular Image either. So you might try to make it resizable, like this:

AsyncImage(url: URL(string: "https://hws.dev/img/logo.png"))
    .resizable()
    .frame(width: 200, height: 200)

…except that won’t work either, and in fact it’s worse because now our code won’t even compile. You see, the modifiers we’re applying here don’t apply directly to the image that SwiftUI downloads – they can’t, because SwiftUI can’t know how to apply them until it has actually fetched the image data.

Instead, we’re applying modifiers to a wrapper around the image, which is the AsyncImage view. That will ultimately contain our finished image, but it will also contain a placeholder that gets used while the image is loading. You can actually see the placeholder just briefly when your app runs – that 200x200 gray square is it, and it will automatically go away once loading finishes.

To adjust our image, you need to use a more advanced form of AsyncImage that passes us the final image view once it’s ready, which we can then customize as needed. As a bonus, this also gives us a second closure to customize the placeholder as needed.

For example, we could make the finished image view be both resizable and scaled to fit, and use Color.red as the placeholder so it’s more obvious while you’re learning.

AsyncImage(url: URL(string: "https://hws.dev/img/logo.png")) { image in
    image
        .resizable()
        .scaledToFit()
} placeholder: {
    Color.red
}
.frame(width: 200, height: 200)

A resizable image and Color.red both automatically take up all available space, which means the frame() modifier actually works now.

The placeholder view can be whatever you want. For example, if you replace Color.red with ProgressView() – just that – then you’ll get a little spinner activity indicator instead of a solid color.

If you want complete control over your remote image, there’s a third way of creating AsyncImage that tells us whether the image was loaded, hit an error, or hasn’t finished yet. This is particularly useful for times when you want to show a dedicated view when the download fails – if the URL doesn’t exist, or the user was offline, etc.

Here’s how that looks:

AsyncImage(url: URL(string: "https://hws.dev/img/bad.png")) { phase in
    if let image = phase.image {
        image
            .resizable()
            .scaledToFit()
    } else if phase.error != nil {
        Text("There was an error loading the image.")
    } else {
        ProgressView()
    }
}
.frame(width: 200, height: 200)

So, that will show our image if it can, an error message if the download failed for any reason, or a spinning activity indicator while the download is still in progress.

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.9/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.