Using custom publishers to drive SwiftUI views
Published on: June 23, 2020In 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?