Exploring Combine’s Publisher: An In-Depth Look at One of the Core Concepts of Apple’s Reactive Framework — Part 2

Ahmad G. Sufi
17 min readApr 11, 2023

--

A comprehensive understanding of how publishers work in the Combine framework

  • What is the Publisher?
  • Publisher protocol in Combine
  • Publishers type in Combine
══════════════════════════════════════
║ Just ║ Future ║ Deferred ║
══════════════════════════════════════
║ Empty ║ Sequence ║ Fail ║
══════════════════════════════════════
║ Record ║
══════════════════════════════════════
  • Binding in SwiftUI
  • SwiftUI and Combine
══════════════════════════════════════
║ @Published ║ @ObservedObject. ║
══════════════════════════════════════
  • How Published and ObservableObject Work Together
  • The Relationship between ObservableObject and the Combine Framework
  • Publishers type in Foundation
══════════════════════════════════════
║ NotificationCenter ║ Result. ║
══════════════════════════════════════
║ URLSession.dataTaskPublisher ║ Timer ║
══════════════════════════════════════
║ publisher on KVO instance Result ║
══════════════════════════════════════

Publishers

is a type that emits a stream of values over time and can be used to represent asynchronous events such as network requests, user inputs, or notifications. Publishers are the core building blocks of the Combine framework and can be combined and transformed to create more complex streams of data.

Publisher protocol in Combine

This code is the definition of the Publisher protocol in Combine Swift. The Publisher protocol is the foundation of the Combine framework, and it defines the behavior of types that emit a stream of values over time.

public protocol Publisher<Output, Failure> {

/// 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

/// Attaches the specified subscriber to this publisher.
///
/// Implementations of ``Publisher`` must implement this method.
///
/// The provided implementation of ``Publisher/subscribe(_:)-4u8kn``calls this method.
///
/// - Parameter subscriber: The subscriber to attach to this ``Publisher``, after which it can receive values.
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}
public protocol Publisher<Output, Failure> 

This declares a protocol named Publisher with two associated types: Output and Failure. Output represents the type of values emitted by the publisher, and Failure represents the type of errors that the publisher can publish.

associatedtype Output
associatedtype Failure : Error

These two lines define the associated types Output and Failure. Output is unconstrained, while Failure is constrained to be a subtype of Error.

func receive<S>(subscriber: S) 
where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input

This is the main method of the Publisher protocol. It's the method that attaches a subscriber to the publisher, allowing the subscriber to receive values emitted by the publisher. The method takes a generic type parameter S that must conform to the Subscriber protocol. The method is also constrained so that the Failure and Output types of the publisher match the Failure and Input types of subscribers.

Just

Just provides a single result and then terminates, providing a publisher with a failure type of <Never>

let just = Just("Hello My Name is Ahmad")

just.sink { value in }

Usage

  • Using catch to handle errors in a one-shot pipeline
  • Using flatMap with a catch to handle errors
  • Declarative UI updates from user input

Future

Represents a single value or an error that will be available in the future. It's a way to encapsulate a piece of work that will complete at some point, and emit its result to subscribers when it's done.

And it is initialized with a closure that eventually resolves to a single output value or failure completion.

The Future the publisher is often used to encapsulate async work that needs to be performed, such as network requests, disk I/O, or expensive calculations. It provides a simple way to wrap up this work in a publisher, which can then be processed and combined with other publishers using Combine operators.

example

Empty

is a type of publisher that doesn't emit any values and immediately completes. It's used to represent a stream of data that has no values to emit, such as a completed network request that didn't return any data.

The Empty the publisher is often used as a placeholder or a default value in Combine pipelines, or as a way to represent an event that has already been completed with no output.

Empty is useful in error-handling scenarios where the value is optional, or where you want to resolve an error by simply not sending anything. Empty can be invoked to be a publisher of any output and failure type combination.

Empty is most commonly used where you need to return a publisher, but don’t want to propagate any values (a possible error-handling scenario). If you want a publisher that provides a single value, then look at Just or Deferred publishers as alternatives.

When subscribed to, an instance of the Empty publisher will not return any values (or errors) and will immediately return a finished completion message to the subscriber.

Since the Empty publisher never emits any values, the receiveValue the closure will never be called. The sink closure's finished the case will be called immediately when the publisher is subscribed, indicating that the publisher has completed successfully with no values.

example

Fail

the Fail is a publisher that immediately fails with a given error, without emitting any values. It's used to represent a stream of data that encountered an error and can't emit any values.

Since the Fail publisher never emits any values, the receiveValue closure will never be called. The sink closure's failure case will be called immediately when the publisher is subscribed to, indicating that the publisher has failed with the specified error.

The Fail publisher is often used as a way to simulate an error in a Combine pipeline or to represent a stream of data that encountered an error and can't emit any values.

Initializing a Fail publisher can be done two ways: with the type notation specifying the output and failure types, or with the types implied by handing parameters to the initializer.

//Initializing Fail by specifying the types
let cancellable = Fail<String, Error>(error: TestFailureCondition.exampleFailure)
// Initializing Fail by providing types as parameters:
let cancellable = Fail(outputType: String.self, failure: TestFailureCondition.exampleFailure)

example

Publishers.Sequence

is a publisher that emits a sequence of values from an underlying Swift sequence. It’s used to represent a stream of data that has a known set of values and can be emitted one value at a time

The sink closure's receiveValue the case will be called for each value in the sequence until the sequence is exhausted and the publisher completes it successfully.

The Sequence the publisher is often used to convert an existing Swift sequence into a Combine publisher or to represent a stream of data that has a known set of values and can be emitted one value at a time.

example

Record

a publisher that emits a sequence of pre-recorded events, such as values and completion events. It’s used to represent a stream of data that has already been recorded, such as from a log file or a network request, and can be played back as a stream of events.

The Record the publisher is often used to test combine pipelines, by recording a sequence of events and replaying them through the pipeline to test its behavior. It can also be used to simulate a pre-recorded stream of data, such as from a log file or a network request.

example

Deferred

The Deferred publisher waits for a subscriber before running the provided closure to create values for the subscriber.

It's useful when you want to defer the creation of a publisher until a subscriber is attached, or when you want to create a new publisher each time a subscriber is attached

let publisher = Deferred {
return Just("Hello, world!")
}

In this example, we’re creating a Deferred publisher that returns a Just publisher that emits a single value (“Hello, world!”). The Deferred publisher won’t create the Just publisher until a subscriber is attached.

When you attach a subscriber to the Deferred publisher, it creates a new publisher and subscribes to it on behalf of the subscriber. This means that each subscriber gets its own instance of the underlying publisher.

The Deferred publisher is often used when you want to avoid creating expensive publishers until they’re actually needed, or when you want to create a new publisher for each subscriber to ensure that they don’t share state.

example

MakeConnectable

A connectable publisher has an explicit mechanism for enabling when a subscription and the flow of demand from subscribers will be allowed to the publisher. By conforming to the ConnectablePublisher protocol, a publisher will have two additional methods exposed for this control: connect and autoconnect. Both of these methods return a Cancellable (similar to sink or assign).

When using connect, the receipt of the subscription will be under imperative control. Normally when a subscriber is linked to a publisher, the connection is made automatically, subscriptions get sent, and demand gets negotiated per the Lifecycle of Publishers and Subscribers. With a connectable publisher, in addition to setting up, the subscription connect() needs to be explicitly invoked. Until connect() is invoked, the subscription won’t be received by the publisher.

let publisher = [1,2,3]
.publisher
.makeConnectable()

publisher.sink { value in
print("Value received 1️⃣ in sink: ", value)
}
.store(in: &cancellables)

publisher.sink { value in
print("Value received 2️⃣ in sink: ", value)
}
.store(in: &cancellables)

The above code will not activate the subscription, and in turn, show any results. To enable the subscription, an explicit connect() is required:

publisher
.connect()
.store(in: &cancellables)

One of the primary uses of having a connectable publisher is to coordinate the timing of connecting multiple subscribers with multicast. Because multicast only shares existing events and does not replay anything, a subscription joining late could miss some data. By explicitly enabling connect()all subscribers can be attached before any upstream processing begins.

In comparison, autoconnect() makes a Connectable publisher acts like a non-connectable one. When you enable autoconnect() on a Connectable publisher, it will automate the connection such that the first subscription will activate upstream publishers.

var cancellables = Set<AnyCancellable>()
let publisher = Just("woot")
.makeConnectable() // 1
.autoconnect() // 2

publisher.sink { value in
print("Value received in sink: ", value)
}
.store(in: &cancellables)
  1. makeConnectable wraps an existing publisher and makes it explicitly connectable
  2. auto-connect automates the process of establishing the connection for you; The first subscriber will establish the connection, subscriptions will be forwarded and demand negotiated.

Both Timer and multicast are examples of connectable publishers.

SwiftUI

The SwiftUI framework is based upon displaying views from an explicit state; as the state changes, the view updates.

SwiftUI uses a variety of property wrappers within its Views to reference and display content from outside of those views. @ObservedObject, @EnvironmentObject, and @Published are the most common that relate to Combine. SwiftUI uses these property wrappers to create a publisher that will inform SwiftUI when those models have changed, creating an objectWillChange publisher. Having an object conform to ObservableObject will also get a default objectWillChange publisher.

SwiftUI uses ObservableObject, which has a default concrete class implementation called ObservableObjectPublisher that exposes a publisher for reference objects (classes) marked with @ObservedObject.

Binding

Binding is a property wrapper in SwiftUI that allows you to create a two-way connection between a value and a view. When you use Binding a value, it creates a reference to the value that can be passed around to different views, and any changes made to the value Binding are automatically propagated back to the original value.

@propertyWrapper public struct Binding<Value> {
public init(get: @escaping () -> Value, set: @escaping (Value) -> Void)
public var wrappedValue: Value { get nonmutating set }
public var projectedValue: Binding<Value> { get }
}

There are a number of SwiftUI property wrappers that create bindings:

  • @State: creates a binding to a local view property, and is intended to be used only in one view
  • @Binding: is used to reference an externally provided binding that the view wants to use to present itself. You will see there upon occasion when a view is expected to be a component, and it is watching for its relevant state data from an enclosing view.
  • @EnvironmentObject: make the state visible and usable across a set of views. @EnvironmentObject is used to inject your own objects or state models into the environment, making them available to be used by any of the views within the current view hierarchy.
  • @Environment is used to expose environmental information already available from within the frameworks

Example of a State that is using a Binding

struct ContentView: View {
@State var name: String = "John"

var body: some View {
VStack {
Text("Hello, \(name)!")
TextField("Enter your name", text: $name)
}
}
}

In this example, we’re using Binding to connect the value of the TextField to the name property of the ContentView. By passing $name as the text argument, we're creating a two-way connection between the TextField and the name property. Whenever the user types in the TextField, the name property is automatically updated, and whenever the name property is updated elsewhere in the view, the TextField is automatically updated to reflect the new value.

Binding is a powerful tool for building reactive user interfaces in SwiftUI, as it allows you to create flexible and dynamic views that can adapt to changes in user inputs or data sources.

SwiftUI and Combine

All of this detail on Binding is important to how SwiftUI works, but irrelevant to Combine — Bindings are not combine pipelines or structures, and the classes and structs that SwiftUI uses are directly transformable from Combine publishers or subscribers.

SwiftUI does, however, use combine in coordination with Bindings. Combine fits into SwiftUI when the state has been externalized into a reference to a model object, most often using the property wrappers @ObservedObject to reference a class conforming to the ObservableObject protocol. The core of the ObservableObject a protocol is a combine publisher objectWillChange, which is used by the SwiftUI framework to know when it needs to invalidate a view based on a model changing.

The objectWillChange publisher only provides an indicator that something has changed on the model, not which property, or what changed about it. The author of the model class can "opt-in" properties into triggering that change using the @Published property wrapper. If a model has properties that aren’t wrapped with @Published, then the automatic objectWillChange notification won’t get triggered when those values are modified. Typically the model properties will be referenced directly within the View elements. When the view is invalidated by a value being published through the objectWillChange publisher, the SwiftUI View will request the data it needs, as it needs it, directly from the various model references.

The other way that Combine fits into SwiftUI is the method onReceive, which is a generic instance method on SwiftUI views.

onReceive can be used when a view needs to be updated based on some external event that isn’t directly reflected in a model’s state is updated.

@Published

@Published in SwiftUI uses an CurrentValueSubject under the hood to implement its functionality.

A CurrentValueSubject is a type of Combine subject that has a current value and publishes changes to that value. When you mark a property @Published in SwiftUI, you're creating a CurrentValueSubject with the property's initial value and using it to publish changes to the property.

This allows SwiftUI to automatically update views when the @Published property changes, making it easy to build reactive UIs.

When a property is marked with @Published, the SwiftUI framework automatically synthesizes an objectWillChange publisher that emits a signal before the value of the property changes.

The publisher’s output type is inferred from the type of the property, and the error type of the provided publisher is <Never>

Using @Published should only be done within reference types - that is, within classes.

'wrappedValue' is unavailable: @Published is only available on properties of classes

ObservableObject

ObservableObject is a protocol in SwiftUI and Combine that defines an object whose properties can be observed for changes. A type that conforms to ObservableObject can be observed by one or more views or other objects, allowing it to notify interested parties when its state changes.

In SwiftUI, ObservableObject is used as the basis for the view model pattern, where you create a separate object to hold your app's state and logic, and then pass this object to your views. The views can then observe the object for changes and update themselves accordingly.

For an object to conform to ObservableObject, it must:

  1. Inherit from the NSObject class.
  2. Declare the properties that should be observed using the @Published property wrapper.
  3. Ensure that any changes to the observed properties are made on the main thread.
class MyModel: ObservableObject {
@Published var name: String = "Alice"
@Published var age: Int = 30
}

struct MyView: View {
@ObservedObject var model: MyModel

var body: some View {
VStack {
TextField("Name", text: $model.name)
Stepper(value: $model.age, in: 0...100) {
Text("Age: \(model.age)")
}
}
}
}

In this example, MyModel is an ObservableObject with two properties, name and age, both marked with @Published.

The MyView displays a text field and a stepper that is bound to the name and age properties of the MyModel object using the $ syntax.

When the user enters a new name or changes the age using the stepper, the @Published properties are updated, and the objectWillChange publisher of the MyModel object emits a signal that triggers an update to the UI.

How Published and ObservableObject work together?

@Published and ObservableObject work together in SwiftUI to enable reactive programming.

ObservableObject is a protocol that defines an object that can be observed by a view. Any object that conforms to ObservableObject can be used as a source of data in SwiftUI.

On the other hand, @Published is a property wrapper that is used to declare a property as a publisher of changes. It's used inside a class or struct that conforms to ObservableObject. Marking a property with @Published, we're telling SwiftUI that the property is a source of truth, and any changes to this property should trigger an update to the views that depend on it.

Here’s an example:

class MyViewModel: ObservableObject {
@Published var myValue: Int = 0
}

In this example, we declare a class MyViewModel that conforms to ObservableObject. Inside the class, we declare a property myValue and mark it with @Published. This means that myValue is now a publisher of changes, and any changes to it will be propagated to the subscribers (i.e., views that depend on it).

When we use an instance of MyViewModel in a view, we can use the @ObservedObject property wrapper to observe changes to the object. Here's an example:

struct MyView: View {
@ObservedObject var viewModel = MyViewModel()

var body: some View {
Text("My value is \(viewModel.myValue)")
}
}

In this example, we declare a MyView struct that uses an instance of MyViewModel as its data source using the @ObservedObject property wrapper. Any changes made to the myValue property of MyViewModel will automatically trigger an update to MyView because of the @Published property wrapper.

In summary, @Published and ObservableObject work together to enable reactive programming in SwiftUI. @Published is used to mark a property as a publisher of changes, and ObservableObject is used to define an object that can be observed by a view.

What is the relation between ObservableObject and the combine framework?

ObservableObject is a protocol in the SwiftUI framework that provides the ability to observe changes in an object's properties. However, it also has a relationship with the Combine framework in that it can be used in conjunction with Combine's publishers and subscribers to create reactive data models.

When a property is marked with the @Published property wrapper in an object conforming to ObservableObject changes, it emits a CurrentValueSubject from Combine. The CurrentValueSubject is a type of publisher that emits the most recent value of the wrapped property and all future values as they change.

This means that objects conforming to ObservableObject can be used as publishers in Combine, and their properties can be subscribed to by other objects in the app. This allows for a reactive data flow, where changes to the model trigger updates in the UI and other parts of the app.

In addition, the @ObservedObject and @EnvironmentObject property wrappers in SwiftUI can be used to subscribe to objects conforming to ObservableObject. This provides a way to automatically update views when the underlying data changes.

Overall, the combination of ObservableObject and Combine allows for powerful and flexible reactive programming in SwiftUI apps.

Foundation

NotificationCenter

Foundation’s NotificationCenter added the capability to act as a publisher, providing Notifications to pipelines.

NotificationCenter provides a publisher upon which you may create pipelines to declaratively react to application or system notifications. The publisher optionally takes an object reference which further filters notifications to those provided by the specific reference.

Notifications are identified primarily by name, defined by a string in your code, or a constant from a relevant framework.

example

Timer

Foundation’s Timer added the capability to act as a publisher, providing a publisher to repeatedly send values to pipelines based on a Timer instance.

Timer.publish returns an instance of Timer.TimerPublisher. This publisher is connectable, conforming to ConnectablePublisher. This means that even when subscribers are connected to it, it will not start producing values until connect() or autoconnect() is invoked on the publisher.

The publisher has an output type of Date and a failure type of <Never>.

If you want the publisher to automatically connect and start receiving values as soon as subscribers are connected and make requests for values, then you may include autoconnect() in the pipeline to have it automatically start to generate values as soon as a subscriber requests data.

example 1

Alternatively, you can connect up the subscribers, which will receive no values until you invoke connect() on the publisher, which also returns a Cancellable reference.

example 2

KeyValueObserving

is a mechanism in Cocoa and Cocoa Touch that allows objects to be notified when a property of another object changes. In Combine, KVO can be used as a source of values for publishers using the NSObject.publisher(for:options:) method.

This method returns a publisher that emits a stream of new values each time the observed object’s property changes. The emitted value is a tuple of the observed object and the change dictionary, which describes the new and old values for the property.

Here’s an example of using KVO to observe changes to a property on an instance of a Person class

Note that for KVO to work, the observed object’s properties must be marked as @objc dynamic. Also, keep in mind that KVO only works with objects that inherit from NSObject.

URLSession.dataTaskPublisher

URLSession.dataTaskPublisher is a publisher provided by Combine that allows you to make network requests and receive the response data as a stream of values. This publisher creates a data task for a URL request and emits a tuple of Data and URLResponse objects upon successful completion of the task. If the task fails with an error, it emits the error instead.

Here’s an example of usage

Result

Result is a type in the Combine framework that represents the result of an operation that can either succeed or fail with an error. It's an enum with two cases: .success and .failure.

The .success case holds an associated value of the successful result type, while the .failure case holds an associated value of the failure error type.

The Result type can be used to transform asynchronous operations that return a completion block with success and error parameters into a Publisher that emits either a successful value or an error.

If you have any questions or comments on this tutorial, do not hesitate to contact me: Linkedin, Twitter, or Email: alsofiahmad@yahoo.com.

Thanks for reading!😀

--

--

No responses yet