Apps often require separate loading, loaded, empty and error views. With SwiftUI we can solve this problem generically and give any view the ability to represent these states.
The loading state
Presenting a shimmering skeleton of the expected data during loading has become an increasing popular design (see the Facebook, Yelp and Meetup apps for example). Facebook provides a popular pod FBShimmering to to help you achieve this effect in UIKit, although it requires that you provide loading images for all of the possible placeholders.
The redacted modifier
With iOS 14 and the
redacted modifier SwiftUI allows you to generate loading placeholders with a single line of code. Since these placeholders are dynamically generated, they scale to any size and device orientation, allowing for seamless placeholder generation across MacOS, iPadOS, and iOS, which is a huge time savings over having to write code to dynamically generate images or making static images for each possible resolution every time your design changes.
For example adding the modifier
.redacted(reason: .placeholder) to the
HStack in the code below transforms each cell in the list into a placeholder.
Creating a shimmer ViewModifier
redacted views don’t shimmer, but SwiftUI makes it easy to write our own shimmer
The modifier simply creates a back and white
LinearGradient with black on the left and right and white in the middle that is vertically centered offscreen to the right. Over the course of 2 seconds it moves until it is vertically centered offscreen to the left. By using the
.screen blendmode we can add the luminosity of the gradient to the underlying view, which gives us the shimmering effect.
We can now apply the
shimmer modifier to our redacted view and instantly we have a loading state with just two lines of code!
However, with SwiftUI and Combine we can do even better than this and add generic loading, error and empty views for any list view.
Modeling loading states
A core tenant of SwiftUI is composition, the idea that we can build up our views from generic reusable components. A single app may make dozens of network requests, each of which requires a loading view. Furthermore each network request could fail, requiring an error view and its possible that a request could return an empty collection, where good design generally dictates that you show an empty state view instead of a blank screen. In Swift, heterogenous types are best represented as an
enum and we can create an
CollectionLoadingState to represent the various possibilities.
Combine allows us to represent a network request as a
Publisher . We can extend publisher to lift any
Output is a
Collection into a
Publisher<CollectionLoadingState<Output, Error>, Never> generically.
This consists of handling each of the possible states:
- We always begin in the
.loadingstate, which we can insure by using
prependto always emit the loading state first.
- If we get an
Outputfrom the publisher, we can use
mapand inspect the collection to see if it is empty and transform the
- Finally if we receive an error, we can use
.catchto replace the failing publisher with one that always emits the
.errorstate instead. By materializing the error we insure that the publisher never fails, since failure is now always captured by our
Generic View Composition
Now that we know how to get a
CollectionLoadingState, we can make a
SwiftUI.View that can generically display a
CollectionLoadingState . All that our view needs to do is switch over the state and supply the view and animations for each possible state:
I’ve made the
shimmer modifier and the
CollectionLoadingState into its own swift package that you can import into your project. The sample project includes an example app that uses the code in this article. You can easily extend this idea to generically model other states (single values, search, submit buttons, paginated collections, etc.)
Generic compositional design allows us to identify a common problem, and extract out its common logic, while leaving behind generic parameters that can be used to customize particular parts of our solution. Its a powerful pattern that lets us reuse code, increase maintainability, consolidate side effects, and build great infrastructure while still providing the flexibility for customization.