Using generics and descriptors to standardise icons, images and placeholders on iOS
Consistency is key when creating a good user experience. When it comes to displaying icons, product photos, placeholders or loading spinners, having a single place to define what they look like and how they should be displayed on screen allows us to keep things consistent.
In the world of iOS we work with
UIImageView, which allows us to display images and configure things like
backgroundColor and let’s not forget
accessibilityIdentifier for those all-important UI tests.
We’re striving for two things:
- A single place to define how we display standard icons and images across our app, including the image view settings that should be applied for each of them.
- A clean call site to apply these images, not having to worry about the specifics of how and when they’re loaded and displayed.
Here are a few examples of the common image types we’re looking to standardise:
- Loading spinner: local image, centralised with no scaling, light grey background, ‘loadingImage’ accessibility identifier.
- Downloaded product image: downloaded from URL, scaled to fill, ‘productImage’ accessibility identifier.
- Placeholder image: local image, centralised with no scaling, grey tint colour, light grey background, ‘placeholderImage’ accessibility identifier.
- Download failure icon: local image, centralised with no scaling, red tint colour, light red background, ‘failureImage’ accessibility identifier.
- Feature icons: local image, centralised with no scaling, dark grey tint colour, light grey background, ‘featureX’ accessibilityIdentifier.
Some image/networking libraries allow you to provide a placeholder when loading a remote image, but we need more control than they usually provide to allow us to specify exactly how they’re displayed.
When displaying an image for a particular screen, we don’t want to be concerned with how and when the image is downloaded. We just want to say to the image view: ‘show this icon’, or ‘load this image using our standard placeholders’. We want our call sites to be as simple as possible.
Configuration > Customisation
An initial thought process could be to simply subclass or extend
UIImageView. You could then implement specific methods to display your app’s icons and images, applying the correct image view properties as needed.
While this keeps everything in one place and provides reusability across a single project, this so-called customisation is limited in its capability. It can lead to bloated classes with code that’s not easily testable nor portable across projects.
In a more stricter sense, the image view shouldn’t know what it’s displaying. Its interface/domain should be clear of business-related concepts.
Views should be dumb. They should be configured, but they shouldn’t have any knowledge of what a particular configuration means or represents.
As a general rule, the more code you write that’s not dependant on one specific app’s implementation, the more decoupled your features and components will be, and the easier life will become 🏖
An alternative and better suited solution is a so-called configuration approach. You define type that bundles up all the information needed to describe how images are loaded, set up and displayed, and then create (named) instances of this type to represent your app’s images and icons.
UIImageView can then be extended to give it the ability to read and apply the properties from this single type. This removes any app-specific knowledge from
UIImageView, allowing the code to be reused across projects. It’s also easy to write unit tests to ensure that when various configurations are applied, the image view’s properties are set to the correct values.
We don’t want to be loading a loading image!
Our goal is to encapsulate everything required to display an image, and have a single place where we use it to define each of our app-specific icons and images.
If the image is local it can be displayed right away. If it’s a remote image, we’ll want to provide a loading (and potentially a failure) image to show in its place until it’s downloaded. It’s also important that the loading/failure images are locally available. We don’t want to be loading a loading image!
We’re distinguishing between immediately available images and remote images that require downloading, which we can represent as
To implement the kind of constraints we’re looking for we can take advantage of generic types. We can build a descriptor type that encapsulates the image view properties we need, and also holds an image reference that can be used by the consumer to get hold of an image before showing it on screen. We’ll call this type an
We’re not constraining the
ImageReference placeholder type here, we’ll leave it up to whoever consumes the image descriptor to declare exactly what types of image references it supports. Let’s take a look at how
UIImageView can do this.
UIImageView declares the exact types of image descriptors it supports, which allows it to constrain certain capabilities by specific image reference types. This ensures that while the remote image is being fetched, the loading/failure placeholder images are displayable immediately.
Let’s take a look at the implementation of our
UIImageView extension that applies image descriptors.
We’re effectively asynchronously mapping an ImageDescriptor<URL> to an
We can apply an
ImageDescriptor<UIImage> immediately, but when we’re given an
ImageDescriptor<URL> we have to download the image. While that’s loading, we apply our loading image descriptor, and if the fetch fails, we apply our failure image.
You’ll see that we download images using
UIImageView.fetchImage(...) and cancel any existing downloads with
UIImageView.cancelAnyExistingFetches(). The specifics of these implementations are outside the scope of this post, but they could be anything that allows you to request an image from a URL and cancel any existing requests that are tied to this image view.
As soon as the image is downloaded, we construct a new
UIImage-based image descriptor and apply it. We’re effectively asynchronously mapping an
ImageDescriptor<URL> to an
Now we have all the mechanics in place, it’s time for the fun stuff: defining our own app-specific image descriptors and applying them!
Wherever something expects an
ImageDescriptor<UIImage> we can simply provide
.failure and we’ll be reusing all those carefully configured settings to display the icons. If we want to load and display a product given a URL, we can simply use
.productImage(with: urlToProduct) and all our products will be displayed the same!
Let’s see this in action.
If we’re always using the same loading/failure placeholder images across the app then it’s trivial to add default values to those parameters to avoid that repetition.
It’s now super-easy to display icons, images and placeholders with a single call to
UIImageView.apply(...), and we’ve ensured they’re always displayed consistently across the app. Huge win! 🚀
Let me know if you have any comments, questions or feedback. You can find me on Twitter @mwaterfall