AsyncResourceView — Simplified Resource Loading

Build asynchronous SwiftUI apps

Andreas Link
Better Programming

--

Today we are going to take a look at how we can deal with asynchronous data in SwiftUI applications. Modern apps heavily rely 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).

View Store

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 success or 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 @MainActor annotation:

Testing

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.

View

As we completed the store, let’s continue with the AsyncResourceView that renders its children using the state-specific closures. While the notRequested-, failure- and 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.

AsyncResourceDefaultNotRequestedView

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.

AsyncResourceDefaultLoadingView

In contrast, the default loading view is visible and will indicate progress until either the success or failure-state is entered.

AsyncResourceDefaultFailureView

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.

Previews

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 (i.e., “Hello World”) or an error. The latter will result in the failure state, where we can retry the action without leaving the preview.

Use Case Example: “Color Gallery”

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, i.e., 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.

Conclusion

In this article, I presented the AsyncResourceView, a 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.

Happy Coding.

--

--