SwiftUI introduced an entirely new approach toward user interface design and composition. Although not quite suited for use in projects with huge user bases yet (since it only supports Apple’s latest OS releases), SwiftUI is great for small features that users with older operating systems can live without. Besides, using the new framework in minor features is probably the best way of practicing for a greater adoption of SwiftUI in the future.
In the app I consider my pet project, Space Photos, I recently added a new feature built with SwiftUI. It’s a carousel of previously posted photos, which displays below main photo’s description. Here’s what it looks like.

The images displayed in the carousel are naturally fetched from the web. But to my huge surprise, SwiftUI has no built-in support for remote images. Unlike UIKit’s UIImageView
, you can’t create an empty SwiftUI image view and populate it with an image later.
The good news is, the solution is pretty simple and fits into 50 lines of code. It’s based on some of the patterns you’re (hopefully) already familiar with: observable objects, Combine publishers, and property wrappers. What’s even better is you can easily set placeholder images for use while the actual image is downloading, and in case download fails. Thus, instead of empty space you’ll see a placeholder icon tailored to your specific app.

The first thing to do is define an “image holder” class your other views will observe (therefore it conforms to ObservableObject
).
import Combine
import SwiftUI
@available(iOS 13, *)
final class RemoteImage: ObservableObject {
// 1
@Published var image: Image
private let imageURL: URL
// 2
private var downloader: AnyCancellable?
init(with imageURL: URL) {
self.imageURL = imageURL
self.image = Image(uiImage: Constants.fallbackImage)
}
// 3
private enum Constants {
static var fallbackImage: UIImage { UIImage(named: "my_placeholder_image")! }
}
}
Here’s a few notes:
image
is prepended with the@Published
attribute, which is a property wrapper, to allow objects that make use ofimage
to receive updates wheneverimage
changes;downloader
is the object that actually downloads the image by its URL. I’ll get back to it in a minute;- In the
Constants
enum, I’ve added a locally stored fallback image that’s displayed in place of an actual image in two cases: (1) before download is completed, and (2) if download fails. Here I use the same image for both situations, but you’re free to define two separate ones.
The only part missing is the downloadImage()
method in which the image is fetched from the web. And here it is.
init(with imageURL: URL) {
self.imageURL = imageURL
self.image = Image(uiImage: Constants.fallbackImage)
// 1
downloadImage()
}
private func downloadImage() {
// 2
downloader = URLSession.shared.dataTaskPublisher(for: imageURL) // 3
.map(\.data) // 4
.compactMap(UIImage.init(data:)) // 5
.replaceError(with: Constants.fallbackImage) // 6
.map(Image.init(uiImage:)) // 7
.assign(to: \.image, on: self) // 8
}
- The call to
downloadImage()
is added to the end of the initializer, so download begins as soon as properties are assigned initial values; - It’s important to keep a reference to the publisher—here, I save it to
RemoteImage
’sdownloader
property—otherwise it will be deallocated as soon as the method reaches its end (which is way earlier than the image is downloaded), while the publisher must be alive for as long asRemoteImage
is; - If you’ve ever worked with
URLSession
and, more specifically,URLSessionDataTask
, this line looks pretty familiar, except for one thing: instead ofdataTask(with:completionHandler:)
, I use an equivalent method for Combine,dataTaskPublisher(for:)
. In this method, you don’t handle results in the completion handler, but pass them down the subscription chain; - Data task publisher’s output (received in case of success) consists of two components: the actual response data, and
URLResponse
with meta information about the network call. I’m only interested in the data, so I use Combine’smap(_:)
operator to get rid of everything else; - Since the data received from the web can be corrupted,
UIImage
initialization has a potential to fail. I usecompactMap(_:)
to only publish successfully initialized images; - Every Combine publisher defines two types of values:
Output
andFailure
. The former is what you get when the publisher succeeds, and the latter is for when the publisher fails.Failure
can be of typeNever
if the publisher never fails, and that’s a thing to remember. Up to this point, I focused exclusively on the output, but now it’s time to handle the error case. By callingreplaceError(with:)
, I make sure if an error comes, it’s effectively replaced with a fallback value. From now on, all subsequent publishers will have theNever
type ofFailure
; - Right now, the type of
Output
isUIImage
. It’s not a mistake: SwiftUI’sImage
s cannot be initialized withData
values directly, and that’s another thing I consider SwiftUI design’s flaw. However, themap(_:)
operator effortlessly transformsUIImage
s intoImage
s; - Finally, the resulting value is assigned to
RemoteImage
’simage
property. When it happens, the new value will be published, and every object that usesimage
will receive it. Such a behavior is possible thanks to the@Published
attribute.
When you want to embed an image into your SwiftUI view, there’s just two things to do. Here’s a simple view with just the image.
struct MyView: View {
// 1
@ObservedObject var myImage = RemoteImage(with: URL(string: "https://i.imgur.com/5sgoFsa.jpg")!)
var body: some View {
// 2
myImage.image
}
}
- Add a
RemoteImage
variable to your view and prepend the declaration with the@ObservedObject
attribute. Doing so will ensure the view is invalidated and redrawn whenevermyImage
changes; - Use the
image
property onmyImage
to access theImage
object.
Here’s the complete RemoteImage
code. Just 32 lines.
import Combine
import SwiftUI
@available(iOS 13, *)
final class RemoteImage: ObservableObject {
@Published var image: Image
private let imageURL: URL
private var downloader: AnyCancellable?
init(with imageURL: URL) {
self.imageURL = imageURL
self.image = Image(uiImage: Constants.fallbackImage)
downloadImage()
}
private func downloadImage() {
downloader = URLSession.shared.dataTaskPublisher(for: imageURL)
.map(\.data)
.compactMap(UIImage.init(data:))
.replaceError(with: Constants.fallbackImage)
.map(Image.init(uiImage:))
.assign(to: \.image, on: self)
}
private enum Constants {
static var fallbackImage: UIImage { UIImage(named: "my_placeholder_image")! }
}
}