Using generics and descriptors to standardise icons, images and placeholders on iOS

Michael Waterfall
May 30, 2018 · 5 min read
Image for post
Image for post

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 contentMode, tintColor, backgroundColor and let’s not forget accessibilityIdentifier for those all-important UI tests.

We’re striving for two things:

  1. 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.
  2. 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

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!

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 UIImage and URL respectively.

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 ImageDescriptor.

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 ImageDescriptor<UIImage>

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 ImageDescriptor<UIImage>.

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 .logo .loading or .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.

Image for post
Image for post

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

The ASOS Tech Blog

A collective effort from ASOS's Tech Team, driven and…

Thanks to Peter Goldsmith

Michael Waterfall

Written by

iOS Engineer at @ASOS

The ASOS Tech Blog

A collective effort from ASOS's Tech Team, driven and directed by our writers. Learn about our engineering, our culture, and anything else that's on our mind.

Michael Waterfall

Written by

iOS Engineer at @ASOS

The ASOS Tech Blog

A collective effort from ASOS's Tech Team, driven and directed by our writers. Learn about our engineering, our culture, and anything else that's on our mind.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store