Exploring Combine’s Publisher: An In-Depth Look at One of the Core Concepts of Apple’s Reactive Framework — Part 2
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.
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.
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)
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.
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.
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.
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)
- makeConnectable wraps an existing publisher and makes it explicitly connectable
- 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:
- Inherit from the
NSObject
class. - Declare the properties that should be observed using the
@Published
property wrapper. - 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.
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.
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.
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.