Build asynchronous SwiftUI apps
Today we are going to take a look at how we can deal with asynchronous data in SwiftUI applications. Modern apps rely heavily on resources that are received over the network, and hence may be affected by connectivity issues or data loss. If, for example, you travel by train within Germany, you may be surprised how often you will experience radio gaps or interruptions due to weak cellular reception.
Hence, we as developers have to design our apps to include feedback when an action takes longer than expected and offer the ability to retry the action in case that it failed. This way, we can make our apps stand out, since they can cope with conditions that are far from optimal.
This article introduces the reusable component
AsyncResourceView that abstracts loading as well as failure states when fetching asynchronous data, such that we can focus on features rather than writing repetitive error-prone code.
You can check out the project on GitHub (Link).
First, let’s implement the
AsyncResourceViewStore<Resource> that is responsible for driving the UI. Given the loader, the store initially remains in the
notRequested state until
loadResource is called and the
loading state is entered. Finally, depending on the result of the operation, either the
failure state is entered.
Note that the store is independent of SwiftUI and may be used with an alternative UI framework in the future. In addition, we ensure that state changes only occur on the main thread using the
Even though its implementation looks simple, let’s include unit tests to ensure that we are free to refactor the store in the future without changing its behavior.
First, the store should be in the
notRequested state. The
makeSUT helper instantiates the
AsyncResourceViewStore with a loader stub, such that we have control over its outcome when making assertions about the expected behavior.
Second, we expect the store to enter the
success state when the resource loading succeeded. Similarly, we expect the store to enter the
failure state in case that the resource loading failed.
Finally, we also expect the store to enter the
success state after the resource loading initially failed but later succeeded. This way, we ensure that the user will have the option to retry the action in case that it failed.
As we the store, let’s continue with the
AsyncResourceView that renders its children using the state-specific closures. While the
loading- views are optional, we are required to specify the
success view given the resource. This way, we can break down complexity and only have to deal with a single instead of multiple states at once.
For example, using the
notRequested view, we can specify how the UI should look like until the resource is requested. Note that the default representation is not visible and is only used to trigger the callback as soon as it appeared. Instead, one can also think of a visual representation that features a button to let the user decide when the action is run.
In contrast, the default
loading view is visible and will indicate progress until either the success or failure-state is entered.
Finally, in case no
failure closure exists, the
AsyncResourceDefaultFailureView renders a counterclockwise arrow to retry the action in case that it failed. Note that custom views may also consider the error to provide additional information about why the action did not work as intended.
Undoubtedly, one of the great advantages of SwiftUI over UI Kit is that we can get real-time feedback about how the rendering is composed. This is especially true when dealing with interactive previews that offer great insights into the look and feel of a component. Subsequently, you can find examples for a static as well as interactive preview:
While the static preview renders itself based on the predefined state of the store, the interactive preview explicitly communicates with the loader and waits until the result is made. Since, the loader may fail, we throw a die and either return the resource (ie, “Hello World”) or an error. The latter will result in the failure state, where we can retry the action without leaving the preview.
To visualize how the component is used, let’s implement a color gallery where items are arranged in a three-column grid. Each item features the
AsyncResourceView to request its color from the loader that will either return a random color or fail after [0.3, 3.0] seconds. As stated above, a retry button is shown in case the action failed.
Since we do not specify a custom
notRequested view, the default view is used that requests the resource as soon as it appeared. By wrapping the items in SwiftUI’s
LazyVGrid they are only created when needed.
Each of the items is driven by its own store, ie,
AsyncResourceViewStore that transitions between states depending on how long the action takes.
Finally, we create a
GalleryStore that drives the composition and provides a color for each individual loader.
In this article, I presented the
AsyncResourceViewa consistent way to deal with asynchronous resources in SwiftUI applications.
Using the component, we can avoid repetitive code and spend more time on implementing features rather than writing the same loading- or error handling code throughout the App.