URL Image view in SwiftUI
Views that download image from URL are extremely popular in iOS development. It is very convenient to set a URL on a view and get an image displayed. And not to worry about networking, caching, and synchronization.
SwiftUI came out without native support for downloading images. This presented an opportunity to create one, native to SwiftUI, and without UIView legacy.
After more than a year of development, and two released versions, I have some thoughts why there is no native one. I will keep `em for the end of the article. I will also share how to create one yourself, and pitfalls you may encounter.
But first, let me introduce you to the URLImage package.
Using URLImage
URLImage uses Swift Package Manager so you only need to copy/paste the package URL:
https://github.com/dmytro-anokhin/url-image
in Xcode (File/Swift Packages/Add Package Dependency...
) and import URLImage
module.
You initialize the URLImage
object with a URL of an image and after some time it gets displayed. Similar to how you initialize the Image
object with the name of an image in your app bundle or the system one.
As oppose to standard Image
object, that synchronously fetches the image, URLImage
reflects a state of asynchronous operation. Actually a chain of operations: cache lookup, download, caching, and decoding.
All you need is the URL and a closure to customize presentation of the downloaded image.
In the closure you can add various modifiers specific to the image.
Asynchronous operation can be in the one of four states: empty or not started, in progress, successfully completed, or failed. Each of four states is represented by a view. URLImage
initiates download and switches this views to reflect the state change.
You can provide a view for each state. Some closures contain additional arguments, like download progress, encountered error, or image information.
URLImageOptions and URLImageService
There is a way to customize downloads. Things like when images can be deleted, caching policies, HTTP headers, and many more.
This example sets custom identifier so the image can be looked up in cache using it, rather than the URL.
URLImageService
object provides centralized way to set image options used by default, and also cleanup expired images.
How URLImage works
URLImage utilizes approach in detail described in one of my articles: Building reusable content loading view in SwiftUI.
The idea, in a nutshell, is to load an image in an object that implements ObservableObject
protocol. The view subscribes to this object, using ObservedObject
property wrapper, and reflects the loading state in its body
method. View-model relationship of SwiftUI and Combine.
Here’s how to build one in 5 min or so:
Of course, this approach can be applied to any kind of content.
The question is why we need a package if this can be so simple? Let me explain.
Downloading image data
You may notice that the example uses data task, created by dataTaskPublisher
method. This creates in memory buffer to accumulate data. Unnecessarily using memory, and potentially harming performance, when you have large images.
You may want to use download task, to download an image into a temporary file. However, you must your own Publisher
implementation for URLSessionDownloadTask
since URLSession
doesn’t provide one.
Another case to consider is what if the same image is used in multiple places. In order not to duplicate downloads you can use Publishers.Share
type.
Caching downloaded data
Data task uses URLCache
and, by default, respects HTTP Cache-Control header. This is very useful if you have dedicated CDN and your app can not work without internet connection. Exactly how a web browser do.
You can configure the URLImage package to work like that: Using URLCache.
For offline usage; to create your own expiry date policy, or if you opt to use download task; or if you need to uniquely identify images using identifier, not related to URL; you will have to build you own cache.
When caching files, in order to achieve best lookup performance, you should use a database to keep the index of downloaded files. Core Data is perfect for this task, and URLImage
package describes Core Data model programmatically, so you don’t need to bundle unnecessary resources (also because when the first version was released Swift Packages didn’t support resources).
In-memory cache
You may want to consider second level of caching. In-memory cache provides quick lookup for recently accessed images to reduce disk access. URLImage
uses standard NSCache
for this purpose.
When to start download
In the example playgroundURLImage
loads its content from the initialization method. In SwiftUI view initialization is typically very close in lifecycle to presentation so it is a good place.
However, you may want to use URLImage
inside List
, LazyVStack
, or LazyVGrid
, ie a view that displays a collection of items. This is where it is beneficial to use appearance callbacks (onAppear
/onDisappear
) to start and cancel downloads. Maybe with a small delay to account for user scrolling over a collection of items.
The URLImage
package provides options to control download behaviour: loadImmediately
, loadOnAppear
, and cancelOnDisappear
.
It may be surprising that there is no standard URL image view in SwiftUI. As you can see, it is not hard to build a simple one yourself. What makes it challenging is to tailor the solution for a concrete case.
In some apps you may want to have behaviour as close to web browser as possible. Good example is e-commerce apps, where you have access to proper CDN and control HTTP headers.
Other apps, like content readers or social networks, may need to download and store images for offline usage.
This leads to the second reason: images are very close to UI, however they are still part of data, and ideally images must be downloaded and stored on the data model level.
Anyways, this are just my thoughts, and hopefully we will see native URL image view someday. Till then I’ll I continue developing URLImage
package.
https://github.com/dmytro-anokhin/url-image
Cheers! 👋