Using custom publishers to drive SwiftUI views

Published on: June 23, 2020

In SwiftUI, views can be driven by an @Published property that's part of an ObservableObject. If you've used SwiftUI and @Published before, following code should look somewhat familiar to you:

class DataSource: ObservableObject {
  @Published var names = [String]()
}

struct NamesList: View {
  @ObservedObject var dataSource: DataSource

  var body: some View {
    List(dataSource.names, id: \.self) { name in
      Text(name)
    }
  }
}

Whenever the DataSource object's names array changes, NamesList will be automatically redrawn. That's great.

Now imagine that our list of names is retrieved through the network somehow and we want to load the list of names in the onAppear for NamesList.

class DataSource: ObservableObject {
  @Published var names = [String]()

  let networkingObject = NetworkingObject()
  var cancellables = Set<AnyCancellable>()

  func loadNames() {
    networkingObject.loadNames()
      .receive(on: DispatchQueue.main)
      .sink(receiveValue: { [weak self] names in
        self?.names = names
      })
      .store(in: &cancellables)
  }
}

struct NamesList: View {
  @ObservedObject var dataSource: DataSource

  var body: some View {
    List(dataSource.names, id: \.self) { name in
      Text(name)
    }.onAppear(perform: {
      dataSource.loadNames()
    })
  }
}

This would work and it's the way to go on iOS 13 but I've never liked having to subscribe to a publisher just so I could update an @Published property. Luckily, in iOS 14 we can refactor loadNames() and do much better with the new assign(to:) operator:

class DataSource: ObservableObject {
  @Published var names = [String]()

  let networkingObject = NetworkingObject()

  func loadNames() {
    networkingObject.loadNames()
      .receive(on: DispatchQueue.main)
      .assign(to: &$names)
  }
}

The assign(to:) operator allows you to assign the output from a publisher directly to an @Published property under one condition. The publisher that you apply the assign(to:) on must have Never as its error type. Note that I had to add an & prefix to $names. The reason for this is that assign(to:) receives its target @Published property as an inout parameter, and inout parameters in Swift are always passed with an & prefix. To learn more about replacing errors so your publisher can have Never as its error type, refer to this blog post I wrote about catch and replaceError in Combine.

Pretty cool, right?

Subscribe to my newsletter