Combine & SwiftUI Communication
SwiftUI uses the Combine framework to provide update notifications to views when an observed object is updated. This is usually accomplished by using the ObservableObject
protocol, the @Published
, @ObservedObject
, @State
& @Binding
property wrappers. Let’s first talk about how the Combine framework facilitates communication via obervered streams
Combine Basics
The Combine framework provides a few objects and protocols that allow communication via asynchronous event streams. In the Rx world these streams are called Observables. A publisher object is used to store events that are to be consumed by one or more subscribers.
Some things we can do with Combine
- Generate events and emit events from a publisher to a subscriber
- Process and transform events to new types
- Process events over time
- Handle errors within an event stream
- Create asynchronous endpoints that are exposed as streams
Generate and emit events from a publisher to a subscriber
We can take a few different approaches
- Generate data via the
Just
,Future
,Empty
,Sequence
,Fail
,Deferred
Publisher operators - Use the SwiftUI
@Published
property wrapper in conjunction with theObservableObject
protocol - Send values to a subject; all subscribers of the subject will receive any published events.
The subsriber provides two functions to process any published events:
sink(receiveCompletion:receiveValue:)
which takes as input two closures, one to process any completion events and one to process any received values.assign(to:on:)
sends events to the property specified by the keypath in theto
input parameter on the object specified by theon
input parameter.
The Publisher
protocol is defined as:
public protocol Publisher {
/// The kind of values published by this publisher.
associatedtype Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
associatedtype Failure : Error
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}
Publishers always have two types associated with them; the Output
type and the Failure
type.
The Subscriber
protocol is defined as
public protocol Subscriber : CustomCombineIdentifierConvertible {
/// The kind of values this subscriber receives.
associatedtype Input
/// The kind of errors this subscriber might receive.
///
/// Use `Never` if this `Subscriber` cannot receive errors.
associatedtype Failure : Error
/// Tells the subscriber that it has successfully subscribed to the publisher and may request items.
///
/// Use the received `Subscription` to request items from the publisher.
/// - Parameter subscription: A subscription that represents the connection between publisher and subscriber.
func receive(subscription: Subscription)
/// Tells the subscriber that the publisher has produced an element.
///
/// - Parameter input: The published element.
/// - Returns: A `Demand` instance indicating how many more elements the subcriber expects to receive.
func receive(_ input: Self.Input) -> Subscribers.Demand
/// Tells the subscriber that the publisher has completed publishing, either normally or with an error.
///
/// - Parameter completion: A `Completion` case indicating whether publishing completed normally or with an error.
func receive(completion: Subscribers.Completion<Self.Failure>)
}
The subscriber has an Input
and Failure
associated types. To receive events from a publisher the subscribers’ Input
type must match the publishers’ Output
type and the Failure
types of bothe publisher and subscriber must also match.
Publish and Subscribe to an event stream
To publish a single value and immediately send a completion event we can use the Just
operator
import Foundation
import Combine
let publisher: Just<String> = Just("Hello World")
let subscription: AnyCancellable = publisher
.sink { value in
print("Received value: \(value)")
}
When run this displays:
Received value: Hello World
So we
- Created a publisher of type
Just<String>
and initialized it with an initial value of “Hello World” - Called the
sink
function on the publisher to provide an event stream endpoint. This function returns anAnyCancellable
type that represents the subscription. The subscription can then be added to a collection to track the subscriptions. - Passed a closure that takes a value of type
String
and prints a message
If we wanted to get a count of the characters in the publisher we can use the map
operator`:
import Foundation
import Combine
let publisher: Just<String> = Just("Hello World")
let subscription: AnyCancellable = publisher
.map { value in return value.count }
.sink { value in
print("Received value: \(value)")
}
Which displays
Received value: 11
Subjects
Subjects are objects that are both Publisher
and Subscriber
. If used as a publisher, we can send values to it via the send()
functions. If used as a subscriber, we can pass the subject into the subscribe
method on any publisher. In each case the subject will emit any values sent to it.
import Foundation
import Combine
// the subject will act as a Publisher
let subject: PassthroughSubject<String, Never> = PassthroughSubject<String, Never>()
// subscribe to the subject
let subscription: AnyCancellable = subject
.sink { value in
print("Received value: \(value)")
}
// send events through the publisher to the subscriber
subject.send("Hello World from send()")
// the subject will act as a subscriber, the publisher is Just<String>
let publisher: Just<String> = Just("Hello World from Just<String>")
publisher
.subscribe(subject)
The following is output
Received value: Hello World from send()
Received value: Hello World from Just<String>
In the first case we sent Hello World
to the subject via a call to send()
on the subject. In the second case we subscribed the subject to the publisher. Note that the closure in the sink
call is associated with the subscription which is associated with the subject. The call to subscribe
will hook the publisher with the closure specified in sink
.
@Published
The @Published
property wrapper is used to wrap properties within classes that adopt the ObservableObject
protocol. The @Published
wrapper is defined as:
/// Adds a `Publisher` to a property.
///
/// Properties annotated with `@Published` contain both the stored value and a publisher which sends any new values after the property value has been sent. New subscribers will receive the current value of the property first.
/// Note that the `@Published` property is class-constrained. Use it with properties of classes, not with non-class types like structures.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct Published<Value> {
/// Initialize the storage of the Published property as well as the corresponding `Publisher`.
public init(initialValue: Value)
/// A publisher for properties marked with the `@Published` attribute.
public struct Publisher : Publisher {
/// The kind of values published by this publisher.
public typealias Output = Value
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Never
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where Value == S.Input, S : Subscriber, S.Failure == Published<Value>.Publisher.Failure
}
/// The property that can be accessed with the `$` syntax and allows access to the `Publisher`
public var projectedValue: Published<Value>.Publisher { mutating get }
}
This does not specify the
wrappedValue
like other property wrappers@State
and@Binding
. I’m assuming thewrappedValue
is an internal property of the@Published
property wrapper
This is a property wrapper around the Value
type. To create the property it is required that an initial value be passed to the init method. The wrapper provides an internal Publisher that provides wrapper specific elements (Output
& Failure
typealiases) as well as the Publisher
protocol receive
function. Access to these wrapper specific elements are through the $
accessor. The $
accessor should be prefixed to the @Published
property
class ViewModel : ObservableObject {
@Published var names: [String] = ["Maxine", "Heidi", "Leilani"]
}
let viewModel = ViewModel()
// To access the wrapped value
var names: [String] = viewModel.names
// To access the publisher
var namesPublisher: Published<[String].Publisher = viewModel.$names
Note that
@Published
properties can be subscribed to but they send afinished
event after the initial value is emitted.
import SwiftUI
import Combine
struct ContentView: View {
@ObservedObject var viewModel: ContentViewModel
private var cancellableSet = Set<AnyCancellable>()
var body: some View {
VStack {
Text(viewModel.text)
HStack{
Button(action: {
self.viewModel.status.toggle()
print("set status")
let text = (self.viewModel.status == false) ? "Hello, World!" : "Goodbye"
self.viewModel.text = text
print("text set")
}) {
Image(systemName: "person.badge.plus.fill")
.font(.largeTitle)
}
}
}
}
init(viewModel: ContentViewModel) {
self.viewModel = viewModel
let id = "\(Date()) ContentView.init"
self.viewModel.text
.publisher
.receive(on: DispatchQueue.main)
.print("\(id) text")
.map{
$0.uppercased()
}
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("\(id) text Something went wrong: \(error)")
case .finished:
print("\(id) text Received Completion")
}
}, receiveValue: { value in
print("\(id) text Received value (sink) \(value)")
})
.store(in: &cancellableSet)
self.viewModel.objectWillChange
.receive(on: DispatchQueue.main)
.print("\(id) objectWillChange")
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("\(id) objectWillChange Something went wrong: \(error)")
case .finished:
print("\(id) objectWillChange Received Completion")
}
}, receiveValue: { value in
print("\(id) objectWillChange Received value (sink) \(value)")
})
.store(in: &cancellableSet)
}
}
class ContentViewModel: ObservableObject {
@Published var text: String {
willSet {
print("will set text ")
}
}
@Published var status: Bool {
willSet {
print("will set status ")
}
}
init(text: String, status: Bool = false) {
self.text = text
self.status = status
}
}
Here we are subscribing to the @Published var text
property of the view model. The text
property starts with an initial value of “Hello World!”. Once the subscription is created, the property will emit each character in the initial value via its publisher
. Once all characters have emitted, a finish
event is sent which cancels the subscription. Once the subscription is cancelled, no more events are sent from the property.
Once the property value has been sent a finished event is sent. The subscriotion is cancelled after receiving a
finished
event. So you’ll only receive the initial property value. Any subsequent property mutations will not have a subscriber./// Properties annotated with `@Published` contain both the stored value and a publisher which sends any new values after the property value has been sent. New subscribers will receive the current value of the property first.
ObservableObject
To access any change updates from clients of objects that contain @Published
properties the container class must derive from ObservableObject
If the class does not adopt
ObservableObject
no@Published
property change updates are sent
/// A type of object with a publisher that emits before the object has changed.
///
/// By default an `ObservableObject` will synthesize an `objectWillChange`
/// publisher that emits before any of its `@Published` properties changes:
///
/// class Contact: ObservableObject {
/// @Published var name: String
/// @Published var age: Int
///
/// init(name: String, age: Int) {
/// self.name = name
/// self.age = age
/// }
///
/// func haveBirthday() -> Int {
/// age += 1
/// return age
/// }
/// }
///
/// let john = Contact(name: "John Appleseed", age: 24)
/// john.objectWillChange.sink { _ in print("\(john.age) will change") }
/// print(john.haveBirthday())
/// // Prints "24 will change"
/// // Prints "25"
///
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ObservableObject : AnyObject {
/// The type of publisher that emits before the object has changed.
associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never
/// A publisher that emits before the object has changed.
var objectWillChange: Self.ObjectWillChangePublisher { get }
}
Note that the protocol derives from AnyObject
The
ObservableObject
protocol is class only protocol so it cannot be adopted by enums or structs.
The adopter of ObservableObject
will have an objectWillChange
variable which is of type ObservableObjectPublisher
. Since the objectWillChange
is a Publisher
you can call sink
on it to process the event sent by objectWillChange
which will emit a void
event to signal the subscriber that the object will change.
It will not inform the subscriber exactly which property on the object changed, only that the object has changed. A separate subscription for each
@Published
property on the object would be required to listen for changed values to the published properties.
The ObservableObjectPublisher
is defined as
final public class ObservableObjectPublisher : Publisher {
/// The kind of values published by this publisher.
public typealias Output = Void
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Never
public init()
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
final public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == ObservableObjectPublisher.Failure, S.Input == ObservableObjectPublisher.Output
final public func send()
}
Here we see that the ObservableObjectPublisher
sets the associated type Output
equal to Void
. This publisher will only emit an event of type Void
. So to determine if the object will change, attach a subscriber to the objectWillChange
property of the ObservedObject
adopted class.
To get notified whenever the
@ObservableObject
class changes, subscribe to itsobjectWillChange
publisher.
ObservedObject
To update all views within a SwiftUI view hierarchy when an @Published
property on an object changes we will need to provide an @ObservedObject
property wrapper around the property of the ObservableObject
adopted object.
If the
@ObservedObject
property wrapper is not present, updates can still be made to theObservableObject
derived objects’@Published
wrapper properties, but any SwiftUI views that reference the properties will not get updates. It will be Combine only behavior not SwiftUI behavior
Recap
To access @Published
properties you will need for the containing class to adopt the ObservableObject
protocol. You can then either update the properties directly, or access the Published
properties via the $
syntax. If updates need to be sent to swiftUI views, the containing class property must be wrapped in the @ObservedObject
property wrapper.
- Create a class that will contain any properties that need to get observed. The class should adopt
ObservableObject
- Add any properties that need to get observed to the class as
@Published
property wrappers - Any clients of the class should add a property wrapper
@ObservedObject
to the property declaration - To update the
@Published
properties just set them as normal, to retrieve thePublisher
part of the@Published
property, access it via the$
syntax prefixed before the property name.
Example
import Foundation
import Combine
import SwiftUI
class ViewModel : ObservableObject {
@Published var names: [String] = ["Maxine", "Heidi", "Leilani"]
// To access the publisher
var namesPublisher: Published<[String]>.Publisher {
return self.$names
}
private var cancellableSet = Set<AnyCancellable>()
init() {
let subscription: AnyCancellable = self.namesPublisher
.print("ViewModel.namesPublisher")
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("finished")
case .failure(let never):
print(never)
}
}, receiveValue: { value in
print("Received names: \(value)")
})
// store the subscription so that it doesn't go out of scope when init exits
// which will call subscription.cancel() and will cancel the subscription.
subscription.store(in: &cancellableSet)
}
}
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
// To access the publisher
var namesPublisher: Published<[String]>.Publisher {
return viewModel.$names
}
private var cancellableSet = Set<AnyCancellable>()
init() {
let subscription: AnyCancellable = self.namesPublisher
.print("ContentView.namesPublisher")
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("finished")
case .failure(let never):
print(never)
}
}, receiveValue: { value in
print("Received names: \(value)")
})
// store the subscription so that it doesn't go out of scope when init exits
// which will call subscription.cancel() and will cancel the subscription.
subscription.store(in: &cancellableSet)
}
var body: some View {
VStack {
List(viewModel.names, id:\.self){ name in
Text(name)
}
Button(action: {
self.viewModel.names.append("David")
}, label: {
Text("Add Name")
})
}
}
}
Process and transform events to new types
We can transform and perform operations on the emitted events by using the map
, flatMap
, filter
, reduce
operators. The operators are provided by the publisher. Other operators also exist like filter
, debounce
& receive
The protocol Publisher
is adopted by any operator and the enum Publishers
provides a namespace for types that serve as publishers. Each operator returns an appropriate Publishers
enum value. The following categoris of operators are available:
- Convenience Publishers
- Subscriber operators
- Mapping Elements
- Filtering Elements
- Reducing Elements
- Applying Mathematical Operations on Elements
- Applying Matching Criteria to Elements
- Applying Sequence Operations to Elements
- Combining Elements from Multiple Publishers
- Handling Errors
- Adapting Publisher Types
- Controlling Timing
- Creating Reference-type Publishers
- Encoding and Decoding
- Identifying Properties with Key Paths
- Using Explicit Publisher Connections
- Working with Multiple Subscribers
- Buffering Elements
- Adding Explicit Connectability
- Debugging
Each operator is implemented as protocol extension function on the Publisher
protocol. Each operator also returns a struct that extends the Publishers
enum.
/// A namespace for types related to the Publisher protocol.
///
/// The various operators defined as extensions on `Publisher` implement their functionality as classes or structures that extend this enumeration. For example, the `contains()` operator returns a `Publishers.Contains` instance.
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public enum Publishers {
}
The map operator declaration is
extension Publisher {
/// Transforms all elements from the upstream publisher with a provided closure.
///
/// - Parameter transform: A closure that takes one element as its parameter and returns a new element.
/// - Returns: A publisher that uses the provided closure to map elements from the upstream publisher to new elements that it then publishes.
public func map<T>(_ transform: @escaping (Self.Output) -> T) -> Publishers.Map<Self, T>
}
The return value Publishers.Map<Self, T>
is defined as
struct Map<Upstream, Output> where Upstream : Publisher
when the map
function is called on a publisher, the map
function returns a struct that extends the Publishers
enum. This is the definition of the struct returned from the map
operator function:
extension Publishers {
/// A publisher that transforms all elements from the upstream publisher with a provided closure.
public struct Map<Upstream, Output> : Publisher where Upstream : Publisher {
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The closure that transforms elements from the upstream publisher.
public let transform: (Upstream.Output) -> Output
public init(upstream: Upstream, transform: @escaping (Upstream.Output) -> Output)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where Output == S.Input, S : Subscriber, Upstream.Failure == S.Failure
}
}
If you browse through the Combine code, you’ll notice that every object the operators return implements the following:
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
This function is used to attach a subscriber to every operator. This is required by all operators that adopt the Publisher
protocol.