Daniel Tull: Blog

Creating a SwiftUI view for loading Combine publishers

Monday, 17 February 2020

When making views that display information from the internet, a common pattern is to have a view that displays three states: an initial view while content is being fetched and then either the view to display the content or a view to show there was an error.

In this post we’ll walk through creating a SwiftUI view to load content using a Combine publisher, showing views provided by the user for our three possible states; initial, output and failure. This PublisherView will reduce the complexities involved to a single initialiser at the call site.

PublisherView(publisher: publisher,
              initial: { return someInitialView() },
              output: { value in someView(using: value) },
              failure: { error in someView(displaying: error) })

The code in this post is in my PublisherView Swift package on GitHub.

Defining our PublisherView

We’ll make a view that is generic over the publisher and displays different views for its output or failure, as well as defining the view to show for the initial state while the we wait for the publisher to receive its first output value.

struct PublisherView<Publisher, InitialView, OutputView, FailureView>
  where
  Publisher: Combine.Publisher,
  InitialView: View,
  OutputView: View,
  FailureView: View
{
  typealias Output = Publisher.Output
  typealias Failure = Publisher.Failure
}

We define the publisher to be generic, so that we can pass any instance of a publisher in, but have access to its specified Output and Failure types, which we’ll make use of later in this post. Using Publisher as the name for this generic parameter reads nicer in the rest of the code, but because we want to constrain it to conform to the Publisher protocol in the Combine framework, we need to reference the protocol using its fully qualified name: Combine.Publisher. It’ll be the only place we need to reference the Combine protocol, so in my mind it’s better than naming our generic parameter P or some such.

We also define three generic types InitialView, OutputView and FailureView which are all types of View. These are the views that will be displayed for each of our given states.

Display State

To model the different possible states we will use an enum with the three cases each having an associated value, allowing us to specify that we only want to show one single view at any time and that could be one of either the initial, output or failure views. The Display type is nested in the PublisherView gaining access to all of its generic parameters, so we don’t need to duplicate them which is nice for both brevity and understanding.

extension PublisherView {

  fileprivate enum Display {
    case initial(InitialView)
    case output(OutputView)
    case failure(FailureView)
  }
}

Subscribing to the Publisher

When subscribing to publishers we need to keep a strong reference to the returned Cancellable object. When it is released, the publisher will be cancelled and no longer output values.

Attempting to subscribe using the PublisherView

At first, we might try to have two private variables, one to hold the cancellable and a variable to hold the current Display value. As we want changes to the display to trigger SwiftUI to recreate our view we think we can put it as a @State property. Anytime the display state changes, the view body will be refetched.

struct PublisherView<Publisher, InitialView, OutputView, FailureView>
  where
  Publisher: Combine.Publisher,
  InitialView: View,
  OutputView: View,
  FailureView: View
{
  typealias Output = Publisher.Output
  typealias Failure = Publisher.Failure

  private var cancellable: AnyCancellable?
  @State private var display: Display

  init(publisher: Publisher,
       initial makeInitialView: () -> InitialView,
       output makeOutputView: @escaping (Output) -> OutputView,
       failure makeFailureView: @escaping (Failure) -> FailureView) {

    _display = State(initialValue: .initial(makeInitialView()))
    cancellable = publisher
      .receive(on: DispatchQueue.main)
      .sink(receiveCompletion: { completion in
        guard case .failure(let failure) = completion else { return }
        self.display = .failure(makeFailureView(failure))
      }, receiveValue: { output in
        self.display = .output(makeOutputView(output))
      })
  }
}

It may look odd that we’re using the underscore prefix to set the state property itself for _display. This is because we need the initial view passed in the initialiser to set the initial value. if you try to just set a value for display then the compiler will give quite a confusing error that it is being accessed before initialized, which actually occurs because the State wrapper is not initialised but we are attempting to put a value inside it.

Escaping closure captures mutating ‘self’ parameter

We also see two cases of the compiler error above, which are telling us that we are mutating self by setting the display property from both the receiveCompletion and receiveValue closures. PublisherView is a struct so it cannot mutate its properties unless we’re in a mutating function. The solution to this is to create an ObservableObject.

Fixing these issues by subscribing using an internal class

Instead of trying to mutate the struct, we can instead push the publisher loading logic into a class. Our PublisherView struct can now hold a reference to the class, which is free to mutate itself. SwiftUI provides the @ObservedObject property wrapper to observe changes in objects which publish their changes, causing our view to be refreshed. In the initialiser of the PublisherView we can simply pass all the values through to the Loader.

struct PublisherView<Publisher, InitialView, OutputView, FailureView>
  where
  Publisher: Combine.Publisher,
  InitialView: View,
  OutputView: View,
  FailureView: View
{
  typealias Output = Publisher.Output
  typealias Failure = Publisher.Failure

  @ObservedObject private var loader: Loader

  init(publisher: Publisher,
       initial: () -> InitialView,
       output: @escaping (Output) -> OutputView,
       failure: @escaping (Failure) -> FailureView) {

    loader = Loader(publisher: publisher,
                    initial: initial,
                    output: output,
                    failure: failure)
  }
}

The Loader class will play a similar role to what we attempted to do directly in the view. We’ll store the display state as a variable, but use the @Published property wrapper, which publishes chages whenever the display state variable is changed. Initially we’ll set this to be our initial view using the user-provided function. We subscribe to the publisher on the main thread and update the display when a value or failure completion is received.

extension PublisherView {

  fileprivate final class Loader: ObservableObject {

    @Published var display: Display
    private var cancellable: AnyCancellable?

    init(publisher: Publisher,
         initial makeInitialView: () -> InitialView,
         output makeOutputView: @escaping (Output) -> OutputView,
         failure makeFailureView: @escaping (Failure) -> FailureView) {

      display = .initial(makeInitialView())
      cancellable = publisher
        .receive(on: DispatchQueue.main)
        .sink(receiveCompletion: { completion in
          guard case .failure(let failure) = completion else { return }
          self.display = .failure(makeFailureView(failure))
        }, receiveValue: { output in
          self.display = .output(makeOutputView(output))
        })
    }
  }
}

Conforming to the View protocol

Finally, we’ll conform PublisherView to SwiftUI’s View protocol which requires us to specify a body. At first this seemed like an impossible challenge given that you cannot use switch statements inside a view builder and we cannot return different views depending on the display state because SwiftUI requires the same type returned. This may lead us down the path of AnyView, which is the equivalent of putting all the view information into a black box that is very expensive for SwiftUI to inspect.

Instead, there is one key piece of understanding needed; in SwiftUI Optional is View and if that optional is .none, then SwiftUI doesn’t render anything for it. So instead of attempting to return only one view, we should try to display all three views and supply nil for the ones that do not exist.

View builders also don’t currently support if let syntax, so we can extract this logic into private variables for each of the potential views, the initial, output and failure views, making the type returned for each explict. We can be safe in the knowledge of only one of these properties returning a non-nil value for two important reasons:

Our implementation of the body can now place each of the optional view properties into a ZStack, which means that views will replace views when they load in. For example between the initial and an output display state, SwiftUI will create a diff between them. The ZStack will go from having the initial view and two nil views to one with the output view and two nil views.

extension PublisherView: View {

  var body: some View {
    ZStack {
      initialView
      outputView
      failureView
    }
  }

  private var initialView: InitialView? {
    guard case let .initial(view) = loader.display else { return nil }
    return view
  }

  private var outputView: OutputView? {
    guard case let .output(view) = loader.display else { return nil }
    return view
  }

  private var failureView: FailureView? {
    guard case let .failure(view) = loader.display else { return nil }
    return view
  }
}

Using the PublisherView

As an example of using the PublisherView, we’ll create a PostsView to display a list of posts fetched from a server, which is all defined in a publisher that is in this example is passed in. We’ll also create a nested Content view which will do the actual work of showing the array of posts.

The generated memberwise initialiser of the internal Content view has the function signature ([Post]) -> Content, which is interesting because it matches what the PublisherView wants for the output function. This means we can pass Content.init as the parameter for the output, without seeing any curlies in sight!

We can use this same tactic by passing the initialiser functions for a LoadingView and FailureView, which we’ll show for the initial and failure cases respectively. These could be views defined separately to the PostsView and used across the app for consistency.

Remember when we used a ZStack to contain our views? What’s really cool about SwiftUI is that the user can specify that they would like these transitions to animate by using the .animation() modifier on the PublisherView. SwiftUI will use its diff to fade out the initial view and fade in the output view, all without the PublisherView explicitly handling animations or providing properties to turn animations on; This is a very different world to UIKit!

struct PostsView: View {
  // Get this publisher from somewhere, maybe a data task publisher
  let publisher: AnyPublisher<[Post], Error>
  var body: some View {
    PublisherView(publisher: publisher,
                  initial: LoadingView.init,
                  output: Content.init,
                  failure: FailureView.init)
      .animation(.default)
  }
}

extension PostsView {

  fileprivate struct Content: View {
    let posts: [Post]
    var body: some View {
      List(posts) { post in
        Text(post.title)
      }
    }
  }
}

struct LoadingView: View {
  var body: some View {
    // Some awesome loading view
  }
}

struct FailureView: View {
  let error: Error
  var body: some View {
    Text(error.localizedDescription)
  }
}

Conclusion

It’s now possible to create libraries that can integrate with one another based around the core concepts defined in Combine and SwiftUI and specifically the core types of the Publisher and the View. Together with the Swift Package Manager integration into Xcode, this could very well be an explosion of ideas.

As an example, I’m currently using my Resourceful library to create publishers that fetch and transform data from the network and display these using PublisherView. Yet neither package knows anything about the other. The fundamental types are provided by Apple. It’s easy to see that you could use the code in this post and mix it with another library of your choice which vends a Combine publisher, which may be another networking library or other purposes such as observing AVPlayerItem metadata changes or performing HealthKit queries.