Publishers
For general information about publishers see Publishers and Lifecycle of Publishers and Subscribers.
enum Publishers
enum Publishers
docs: Publishers
A namespace for types that serve as publishers.
Declaration:
enum Publishers
Overview:
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.
Just
A publisher that emits an output to each subscriber just once, and then finishes.
Declaration:
struct Just<Output>
Overview:
You can use a Just
publisher to start a chain of publishers. A Just
publisher is also useful when replacing a value with Publishers.Catch
.
In contrast with
Result.Publisher
, aJust
publisher can’t fail with an error.And unlike
Optional.Publisher
, aJust
publisher always produces a value.
Just
has a failure type of <Never>
, often used within a closure to flatMap
in error handling, it creates a single-response pipeline for use in error handling of continuous values.
Future
A publisher that eventually produces a single value and then finishes or fails.
Declaration:
final class Future<Output, Failure> where Failure : Error
Overview:
Future is a publisher that lets you combine in any asynchronous call and use that call to generate a value or a completion as a publisher. It is ideal for when you want to make a single request, or get a single response, where the API you are using has a completion handler closure.
The obvious example that everyone immediately thinks about is URLSession
. Fortunately, URLSession.dataTaskPublisher
exists to make a call with a URLSession
and return a publisher.
If you already have an API object that wraps the direct calls to URLSession
, then making a single request using Future
can be a great way to integrate the result into a Combine pipeline.
There are a number of APIs in the Apple frameworks that use a completion closure. An example of one is requesting permission to access the contacts store in Contacts
.
An example of wrapping that request for access into a publisher using Future
might be:
import Contacts
let futureAsyncPublisher = Future<Bool, Error> { promise in 1️⃣
CNContactStore().requestAccess(for: .contacts) { grantedAccess, err in 2️⃣
// err is an optional
if let err = err { 3️⃣
promise(.failure(err))
}
return promise(.success(grantedAccess)) 4️⃣
}
}
1️⃣
Future
itself has you define the return types and takes a closure. It hands in aResult
object matching the type description, which you interact.2️⃣ You can invoke the async API however is relevant, including passing in its required closure.
3️⃣ Within the completion handler, you determine what would cause a failure or a success. A call to
promise(.failure(<FailureType>))
returns the failure.4️⃣ Or a call to
promise(.success(<OutputType>))
returns a value.
If you want to wrap an async API that could return many values over time, you should not use Future
directly, as it only returns a single value. Instead, you should consider creating your own publisher based on passthroughSubject
or currentValueSubject
, or wrapping the Future
publisher with Deferred
.
Future
creates and invokes its closure to do the asynchronous request at the time of creation, not when the publisher receives a demand request. This can be counter-intuitive, as many other publishers invoke their closures when they receive demand. This also means that you can’t directly link aFuture
publisher to an operator likeretry
.The
retry
operator works by making another subscription to the publisher, andFuture
doesn’t currently re-invoke the closure you provide upon additional request demands. This means that chaining aretry
operator after Future will not result in Future’s closure being invoked repeatedly when a.failure
completion is returned.The failure of the
retry
andFuture
to work together directly has been submitted to Apple as feedback: FB7455914.The
Future
publisher can be wrapped withDeferred
to have it work based on demand, rather than as a one-shot at the time of creation of the publisher. You can see unit tests illustratingFuture
wrapped withDeferred
in the tests at UsingCombineTests/FuturePublisherTests.swift.
If you are wanting repeated requests to a Future
(for example, wanting to use a retry
operator to retry failed requests), wrap the Future
publisher with Deferred
.
let deferredPublisher = Deferred { 1️⃣
return Future<Bool, Error> { promise in 2️⃣
self.asyncAPICall(sabotage: false) { (grantedAccess, err) in
if let err = err {
return promise(.failure(err))
}
return promise(.success(grantedAccess))
}
}
}.eraseToAnyPublisher()
1️⃣ The closure provided in to
Deferred
will be invoked as demand requests come to the publisher.2️⃣ This in turn resolves the underlying api call to generate the result as a
Promise
, with internal closures to resolve the promise.
Deferred
A publisher that awaits subscription before running the supplied closure to create a publisher for the new subscriber.
Declaration:
struct Deferred<DeferredPublisher> where DeferredPublisher : Publisher
Overview:
Deferred
is useful when creating an API to return a publisher, where creating the publisher is an expensive effort, either computationally or in the time it takes to set up. Deferred holds off on setting up any publisher data structures until a subscription is requested. This provides a means of deferring the setup of the publisher until it is actually needed.
The Deferred
publisher is particularly useful with Future
, which does not wait on demand to start the resolution of underlying (wrapped) asynchronous APIs.
Empty
A publisher that never publishes any values, and optionally finishes immediately.
Declaration:
struct Empty<Output, Failure> where Failure : Error
Overview:
You can create a ”Never” publisher — one which never sends values and never finishes or fails — with the initializer Empty(completeImmediately: false)
.
Empty
is useful in error handling scenarios where the value is an 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.
An example of using Empty
:
let myEmptyPublisher = Empty<String, Never>()
Because the types are not be able to be inferred, expect to define the types you want to return.
Fail
A publisher that immediately terminates with the specified error.
Declaration:
struct Fail<Output, Failure> where Failure : Error
Overview:
Fail
is commonly used when implementing an API that returns a publisher. In the case where you want to return an immediate failure,Fail
provides a publisher that immediately triggers a failure on subscription. One way this might be used is to provide a failure response when invalid parameters are passed. TheFail
publisher lets you generate a publisher of the correct type that provides a failure completion when demand is requested.
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.
For example:
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)
Record
A publisher that allows for recording a series of inputs and a completion, for later playback to each subscriber.
Declaration:
struct Record<Output, Failure> where Failure : Error
Overview:
Record
allows you to create a publisher with pre-recorded values for repeated playback.Record
acts very similarly toPublishers.Sequence
if you want to publish a sequence of values and then send a.finished
completion. It goes beyond that allowing you to specify a.failure
completion to be sent from the recording.Record
does not allow you to control the timing of the values being returned, only the order and the eventual completion following them.
Record
can also be serialized (encoded and decoded) as long as the output and failure values can be serialized as well.
An example of a simple recording that sends several string values and then a .finished
completion:
// creates a recording
let recordedPublisher = Record<String, Never> { example in
// example : type is Record<String, Never>.Recording
example.receive("one")
example.receive("two")
example.receive("three")
example.receive(completion: .finished)
}
The resulting instance can be used as a publisher immediately:
let cancellable = recordedPublisher.sink(receiveCompletion: { err in
print(".sink() received the completion: ", String(describing: err))
expectation.fulfill()
}, receiveValue: { value in
print(".sink() received value: ", value)
})
Record
also has a property recording
that can be inspected, with its own properties of output and completion. Record
and recording
do not conform to Equatable
, so can’t be easily compared within tests. It is fairly easy to compare the properties of output
or completion
, which are Equatable
if the underlying contents (output type and failure type) are equatable.
Note: No convenience methods exist for creating a recording as a subscriber. You can use the receive methods to create one, wrapping a
sink
subscriber.
Publishers.Sequence
A publisher that publishes a given sequence of elements.
Declaration:
struct Sequence<Elements, Failure> where Elements : Sequence, Failure : Error
Overview:
Sequence
provides a way to return values as subscribers demand them initialized from a collection. Formally, it provides elements from any type conforming to theSequence
protocol.
If a subscriber requests unlimited demand, all elements will be sent, and then a .finished
completion will terminate the output. If the subscribe requests a single element at a time, then individual elements will be returned based on demand.
If the type within the sequence is denoted as optional
, and a nil
value is included within the sequence, that will be sent as an instance of the optional type.
Combine provides an extension onto the Sequence
protocol so that anything that corresponds to it can act as a sequence publisher. It does so by making a .publisher
property available, which implicitly creates a Publishers.Sequence
publisher.
let initialSequence = ["one", "two", "red", "blue"]
_ = initialSequence.publisher
.sink {
print($0)
}
}
Publishers.MakeConnectable
A publisher that provides explicit connectability to another publisher.
Creates a or converts a publisher to one that explicitly conforms to the ConnectablePublisher
protocol. The failure type of the publisher must be <Never>
.
Declaration:
struct MakeConnectable<Upstream> where Upstream : Publisher
Overview:
Publishers.MakeConnectable
is a ConnectablePublisher
, which allows you to perform configuration before publishing any elements. Call connect()
on this publisher when you want to attach to its upstream publisher and start producing elements. Use the makeConnectable()
operator to wrap an upstream publisher with an instance of this publisher.
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()
andautoconnect()
. Both of these methods return aCancellable
.
When using connect()
, the receipt of 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.
var cancellables = Set<AnyCancellable>()
let publisher = Just("woot")
.makeConnectable()
publisher.sink { value in
print("Value received in sink: ", value)
}
.store(in: &cancellables)
The above code will not activate the subscription, and in turn show any results. In order 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 the connect()
, all subscribers can be attached before any upstream processing begins.
In comparison, autoconnect()
makes a Connectable publisher act like a non-connectable one. When you enabled 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️⃣
autoconnect()
automates the process of establishing the connection for you; The first subscriber will establish the connection, subscriptions will be forwards and demand negotiated.
Info: Making a publisher connectable and then immediately enabling
autoconnect
is an odd example, as you typically want one explicit pattern of behavior or the other. The two mechanisms allow you to choose which you want for the needs of your code. As such, it is extremely unlikely that you would ever want to usemakeConnectable()
followed immediately byautoconnect()
.
Both Timer
and multicast
are examples of connectable publishers.
SwiftUI
The SwiftUI framework is based upon displaying views from explicit state; as the state changes, the view updates.
SwiftUI uses a variety of property wrappers within its View
s 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 a 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
SwiftUI does this primarily by tracking the state and changes to the state using the SwiftUI struct Binding
. A binding is not a Combine pipeline, or even usable as one. A Binding
is based on closures that are used when you get or set data through the binding. When creating a Binding
, you can specify the closures, or use the defaults, which handles the needs of SwiftUI elements to react when data is set or request data when a view requires it.
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.
when you create:
@State private var exampleString = ""
then: exampleString
is the state itself and the property wrapper creates $exampleString (also known as property wrapper’s projected value) which is of type Binding<String>
.
@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 component, and it is watching for its relevant state data from an enclosing view.@EnvironmentObject
: make 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.
Info: The exception to
@EnvironmentObject
cascading across the view hierarchy in SwiftUI is notably when using sheets. Sheets don’t inherit the environment from the view through which they are presented.
@Environment is
used to expose environmental information already available from within the frameworks, for example:
@Environment(\.horizontalSizeClass) var horizontalSizeClass
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 in to 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
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 being updated.
While there is no explicit guidance from Apple on how to use onReceive
vs. models,
as a general guideline it will be a cleaner pattern to update the model using Combine, keeping the combine publishers and pipelines external to SwiftUI views. In this mode, you would generally let the
@ObservedObject
SwiftUI declaration automatically invalidate and update the view, which separates the model updating from the presentation of the view itself.The alternative ends up having the view bound fairly tightly to the combine publishers providing asynchronous updates, rather than a coherent view of the end state. There are still some edge cases and needs where you want to trigger a view update directly from a publishers output, and that is where
onReceive
is most effectively used.
ObservableObject
A type of object with a publisher that emits before the object has changed.
Declaration:
protocol ObservableObject : AnyObject
Overview:
By default an ObservableObject
synthesizes an objectWillChange
publisher that emits the changed value 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)
cancellable = john.objectWillChange
.sink { _ in
print("\(john.age) will change")
}
print(john.haveBirthday())
// Prints "24 will change"
// Prints "25"
When a class includes a
@Published
property and conforms to theObservableObject
protocol, this class instances will get aobjectWillChange
publisher endpoint providing this publisher. TheobjectWillChange
publisher will not return any of the changed data, only an indicator that the referenced object has changed.
The output type of ObservableObject.Output
is type aliased to Void
, so while it is not nil
, it will not provide any meaningful data. Because the output type does not include what changes on the referenced object, the best method for responding to changes is probably best done using sink
.
In practice, this method is most frequently used by the SwiftUI framework. SwiftUI views use the @ObservedObject
property wrapper to know when to invalidate and refresh views that reference classes implementing ObservableObject
.
Classes implementing ObservableObject
are also expected to use @Published
to provide notifications of changes on specific properties, or to optionally provide a custom announcement that indicates the object has changed.
It can also be used locally to watch for updates to a reference-type model.
@Published
A property wrapper that adds a Combine publisher to any property.
Declaration:
@propertyWrapper struct Published<Value>
Overview:
Publishing a property with the @Published
attribute creates a publisher of this type. You access the publisher with the $
operator, as shown here:
class Weather {
@Published var temperature: Double
init(temperature: Double) {
self.temperature = temperature
}
}
let weather = Weather(temperature: 20)
cancellable = weather.$temperature
.sink() {
print ("Temperature now: \($0)")
}
weather.temperature = 25
// Prints:
// Temperature now: 20.0
// Temperature now: 25.0
When the property changes, publishing occurs in the property’s willSet
block, meaning subscribers receive the new value before it’s actually set on the property. In the above example, the second time the sink executes its closure, it receives the parameter value 25
. However, if the closure evaluated weather.temperature
, the value returned would be 20
.
Important: The
@Published
attribute is class constrained. Use it with properties of classes, not with non-class types like structures.
@Published
is part of Combine, but allows you to wrap a property, enabling you to get a publisher that triggers data updates whenever the property is changed. The publisher’s output type is inferred from the type of the property, and the error type of the provided publisher is <Never>
.
A smaller examples of how it can be used:
@Published var username: String = "" 1️⃣
$username 2️⃣
.sink { someString in
print("value of username updated to: ", someString)
}
$username 3️⃣
.assign(\.text, on: myLabel)
@Published private var githubUserData: [GithubAPIUser] = [] 4️⃣
1️⃣
@Published
wraps the property,username
, and will generate events whenever the property is changed. If there is a subscriber at initialization time, the subscriber will also receive the initial value being set. The publisher for the property is available at the same scope, and with the same permissions, as the property itself.2️⃣ The publisher is accessible as
$username
, of typePublished<String>.publisher
.3️⃣ A Published property can have more than one subscriber pipeline triggering from it.
4️⃣ If you are publishing your own type, you may find it convenient to publish an array of that type as the property, even if you only reference a single value. This allows you represent an "Empty" result that is still a concrete result within Combine pipelines, as
assign
andsink
subscribers will only trigger updates on non-nil
values.
If the publisher generated from @Published
receives a cancellation from any subscriber, it is expected to, and will cease, reporting property changes. Because of this expectation, it is common to arrange pipelines from these publishers that have an error type of <Never>
and do all error handling within the pipelines.
For example, if a sink
subscriber is set up to capture errors from a pipeline originating from a @Published
property, when the error is received, the sink will send a cancel
message, causing the publisher to cease generating any updates on change. This is illustrated in the test testPublishedSinkWithError
at UsingCombineTests/PublisherTests.swift.
Additional examples of how to arrange error handling for a continuous publisher like @Published
can be found at Using flatMap with catch to handle errors.
Foundation
NotificationCenter
Foundation
’s NotificationCenter
added the capability to act as a publisher, providing Notification
s 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.
A number of AppKit controls provide notifications when the control has been updated. For example, AppKit’s TextField
triggers a number of notifications including:
textDidBeginEditingNotification
textDidChangeNotification
textDidEndEditingNotification
extension Notification.Name {
static let yourNotification = Notification.Name("your-notification") 1️⃣
}
let cancellable = NotificationCenter.default.publisher(for: .yourNotification, object: nil) 2️⃣
.sink {
print ($0) 3️⃣
}
1️⃣ Notifications are defined by a string for their name. If defining your own, be careful to define the strings uniquely.
2️⃣ A
NotificationCenter
publisher can be created for a single type of notification,.yourNotification
in this case, defined previously in your code.3️⃣ Notifications are received from the publisher. These include at least their
name
, and optionally aobject
reference from the sending object - most commonly provided from Apple frameworks. Notifications may also include auserInfo
dictionary of arbitrary values, which can be used to pass additional information within your application.
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 ofTimer.TimerPublisher
. This publisher is a connectable publisher, conforming toConnectablePublisher
. This means that even when subscribers are connected to it, it will not start producing values untilconnect()
orautoconnect()
is invoked on the publisher.
Creating the timer publisher requires an interval
in seconds, and a RunLoop
and mode
upon which to run. The publisher may optionally take an additional parameter tolerance
, which defines a variance allowed in the generation of timed events. The default for tolerance
is nil
, allowing any variance.
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.
let cancellable = Timer.publish(every: 1.0, on: RunLoop.main, in: .common)
.autoconnect()
.sink { receivedTimeStamp in
print("passed through: ", receivedTimeStamp)
}
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.
let timerPublisher = Timer.publish(every: 1.0, on: RunLoop.main, in: .default)
let cancellableSink = timerPublisher
.sink { receivedTimeStamp in
print("passed through: ", receivedTimeStamp)
}
// no values until the following is invoked elsewhere/later:
let cancellablePublisher = timerPublisher.connect()
publisher from a KeyValueObserving instance
Foundation
added the ability to get a publisher on any NSObject
that can be watched with Key Value Observing
. To create this publisher, you call the function publisher
on the object, providing it with a single (required) KeyPath
value.
For example:
private final class KVOAbleNSObject: NSObject {
@objc dynamic var intValue: Int = 0
@objc dynamic var boolValue: Bool = false
}
let foo = KVOAbleNSObject()
let _ = foo.publisher(for: \.intValue)
.sink { someValue in
print("value updated to: >>\(someValue)<<")
}
Relying on the interface element’s state to trigger updates into pipelines can lead to your state being very tightly bound to the interface elements, rather than your model. You may be better served by explicitly creating your own state to react to from a
@Published
property wrapper.
URLSession.dataTaskPublisher
Foundation
’s URLSession
has a publisher specifically for requesting data from URLs: dataTaskPublisher
dataTaskPublisher
, on URLSession
, has two variants for creating a publisher. The first takes an instance of URL
, the second URLRequest
. The data returned from the publisher is a tuple of (data: Data, response: URLResponse)
.
let request = URLRequest(url: regularURL)
return URLSession.shared.dataTaskPublisher(for: request)
Result
Foundation
also adds Result
as a publisher.
Combine augments Result
from the swift standard library with a .publisher
property, returning a publisher with an output type of Success
and a failure type of Failure
, defined by the Result
instance.
Any method that returns an instance of Result
can use this property to get a publisher that will provide the resulting value and followed by a .finished
completion, or a .failure
completion with the relevant Error.
Last updated
Was this helpful?