Building reusable content loading view in SwiftUI

Dmytro Anokhin
4 min readSep 3, 2020

Modern app development with SwiftUI and Combine eliminates a lot of boilerplate code. Tools like Playgrounds extend this further to allow quick prototyping. One common issue is to load arbitrary content (JSON or binary) from network and display it in a SwiftUI view. The other day I was looking for a simple yet elegant way to use in quick prototypes. I came up with a reusable view that can load arbitrary content. After a couple of iterations I discovered some interesting tricks to share.

Content Loading Design

Content Loading stages: initial, in progress, success, and failure

Content loading is at least three stage process:

  • Before the loading starts there is the initial moment - after objects were created, and before a trigger to load the content;
  • The actual process of loading content. We need to present some progress or activity indication in the UI;
  • And finally the result, success or failure.

SwiftUI encourages creating small, reusable views, and use composition to create the complete picture. Each stage of the content loading process will require a view. The container view will compose the result.

The content loading process can be represented using an enum with associated values. Swift provides Result type, that could be used to represent completed process. However, I find it more convenient to represent the result as separate success and failure cases. This is due to added support of switch statement in function builders (Xcode 12).

It is easy to see what we are aiming for: the container view can switch over the loading state to provide a corresponding view.

Loading Content in SwiftUI View

You probably know by now that a view in SwiftUI can not load content by itself. This is because SwiftUI views are value types. Content loading requires back and forth communication possible only with reference types.

The way to provide remote content to a SwiftUI view is by using ObservedObject property wrapper and ObservableObject protocol.

ObservableObject protocol synthesizes a publisher that emits before the object has changed.

Typically, you would create a class that confirms to ObservableObject and reference it using ObservedObject property wrapper from your view.

Because we are building a reusable view it makes sense to inject ObservableObject. To start, declare RemoteContent protocol.

Associated type Value is used with generic RemoteContentLoadingState type. load and cancel methods are self-explanatory.

Using associated type creates limitations on how the protocol can be used. To freely declare properties using RemoteContent type it is necessary to apply type erasure technique.

Note, that AnyRemoteContent must also delegate objectWillChange publisher to the RemoteContent object.

Remote Content View

With this objects in place we can build the container view.

RemoteContentView uses closures to provide a view for each of the loading states. Associated values of RemoteContentLoadingState passed as arguments for the respective closures.

Init method takes RemoteContent as the first argument and requires it associated types to match.

The remoteContent property of AnyRemoteContent type uses ObservedObject property wrapper. This invalidates the view whenever the remoteContent changes.

Also, failure state allows to reload content by delegating a closure to it’s respective view.

Concrete Remote Content

We can’t use it just yet. The last piece is to implement RemoteContent.

Swift provides Decodable protocol for JSON and Plist deserializable objects. Let’s implement RemoteContent object that can utilize it.

DecodableRemoteContent uses Combine to load and decode remote content. Combine provides decode<Item, Coder> function and this is basically why init takes two generic arguments type and decoder.

The current loading stage is stored in loadingState property. Published property wrapper used to automatically emit when the property changes.

Actual content loading is a chain of Combine publishers and operators. URLSession exposes a publisher for a data task. The result is decoded and mapped to RemoteContentLoadingState type.

Lastly, receiving and assigning the result value to the property must be on the main thread.

We can also provide JSONDecoder by default using extension.

Extending Remote Content View

Almost there. Remember, SwiftUI encourages creating small, reusable views. You probably have a number of auxilary views, like spinners to indicate activity, or messages to show errors. RemoteContentView can be extended to provide this views as default arguments.

One downside is that this approach requires providing all combinations of generic views to customize individual views. However, this is fairly easy to do, and can be done adhoc.

ActivityIndicator is a custom wrapper for UIActivityIndicator that you can find in the package repository.

Using Remote Content View

Finally, we got to the point where we can see how to use RemoteContentView, in the end this is what matters.

Voilà, the list of posts displayed in the List view. And also activity indication and error handling. Now you can concentrate on implementing the UI for your content. Your reusable content loading view will take care of everything else.

The list of posts loaded in the RemoteContentView

As a summary I would like to highlight how using Swift generics allows creating reusable and customizable views; improvements in function builders allow writing straight forward view builder code; and Combine makes communication between objects simple.

The RemoteContentView is a Swift package available on GitHub: https://github.com/dmytro-anokhin/remote-content-view. It includes RemoteImage to load images and new for iOS 14 redacted placeholder. You can find more examples in provided playgrounds.

Hope you find the article and the package useful. Maybe take a bits of it to improve your SwiftUI views.

Till next time, bye!

--

--

Dmytro Anokhin

iOS Developer, here to share best practices learned through my experience. You can find me on Twitter: https://twitter.com/dmytroanokhin