Swift Combine
A publisher is generating and sending data, operators are reacting to that data and potentially changing it, and subscribers requesting and accepting it.
Swift Operator
map
public func map<T>(_ transform: (Output) -> T) -> Just<T>
Copy the code
let _ = Just(5)
.map { value -> String in
switch value {
case _ where value < 1:
return "none"
case _ where value == 1:
return "one"
case _ where value == 2:
return "couple"
case _ where value == 3:
return "few"
case _ where value > 8:
return "many"
default:
return "some"
}
}
.sink { receivedValue in
print("The end result was \(receivedValue)")
}
Copy the code
Back Pressure
Back pressure Combine is designed such that the subscriber
controls the flow of data
, and because of that it also controlswhat
andwhen
processing happens in the pipeline. This is a feature of Combine called back-pressure.
Publishers
Combine provides a number of additional convenience publishers:
- Just
- Future
- Deferred
- Empty
- Sequence
- Fail
- Record
- Share
- Multicast
- ObservableObject
- @Published
SwiftUI uses the @Published
and @ObservedObject
property wrappers, provided by Combine, to implicitly create a publisher and support its declarative view mechanisms.
Publisher receive(subscriber)
final public func receive<S>(subscriber: S) where S : Subscriber, SubjectType.Failure == S.Failure, SubjectType.Output == S.Input
Copy the code
Publisher receive(on:,options:)
You can use the 'Publisher/receive(on:options:)' operator to receive results and completions on a particular scheduler, such as UI work on the main run loop. Compared to ' 'Publisher/subscribe(on:options:)' 'which affects upstream messages, Publisher/receive(on:options:) ' 'changes the execution context of downstream messages // Specifies the scheduler on which to receive elements from the publisher. You use the ``Publisher/receive(on:options:)`` operator to receive results and completion on a specific scheduler, such as performing UI work on the main run loop. In contrast with ``Publisher/subscribe(on:options:)``, which affects upstream messages, ``Publisher/receive(on:options:)`` changes the execution context of downstream messages. In the following example, the ``Publisher/subscribe(on:options:)`` operator causes `jsonPublisher` to receive requests on `backgroundQueue`, while the ``Publisher/receive(on:options:)`` causes `labelUpdater` to receive elements and completion on `RunLoop.main`. let jsonPublisher = MyJSONLoaderPublisher() // Some publisher. let labelUpdater = MyLabelUpdateSubscriber() // Some subscriber that updates the UI. jsonPublisher .subscribe(on: backgroundQueue) .receive(on: Runloop.main).subscribe(labelUpdater) is more likely to use Publisher/receive(on:options:) /// Prefer when executed by the subscriber ``Publisher/receive(on:options:)`` over explicit use of dispatch queues when performing work in subscribers. For example, instead of the following pattern: /// /// pub.sink { /// DispatchQueue.main.async { /// // Do something. /// } /// } /// /// Use this pattern instead: /// /// pub.receive(on: DispatchQueue.main).sink { /// // Do something. /// } /// /// > Note: Publisher/receive(on:options:) doesn't affect the scheduler used to call the subscriber's ``Subscriber/receive(subscription:)`` method.Copy the code
Specifies the scheduler
public func receive<S>(on scheduler: S, options: S.SchedulerOptions? = nil) -> Publishers.ReceiveOn<Self, S> where S : Scheduler
Copy the code
Publisher subscribe(subject)
> Attaches the specified subject to this publisher.
> Parameter subject: The subject to attach to this publisher.
public func subscribe<S>(_ subject: S) -> AnyCancellable where S : Subject, Self.Failure == S.Failure, Self.Output == S.Output
Copy the code
Built-in Subject
Combine has two built-in themes CurrentValueSubject and PassthroughSubject they behave similarly, the difference being that CurrentValueSubject remembers and requires initial state, PassthroughSubject does not. Both provide updated values to any subscriber when.send() is called
Subscribers to the subscriber
The Combine has two built-in subscribers: Assign and Sink. SwiftUI has one built-in subscriber: onReceive.
assign(to:,on:)
/// Assigns each element from a publisher to a property on an object. /// /// Use the ``Publisher/assign(to:on:)`` subscriber when you want to set a given property each time a publisher produces a value. /// /// In this example, the ``Publisher/assign(to:on:)`` sets the value of the `anInt` property on an instance of `MyClass`: /// /// class MyClass { /// var anInt: Int = 0 { /// didSet { /// print("anInt was set to: \(anInt)", terminator: "; ") /// } /// } /// } /// /// var myObject = MyClass() /// let myRange = (0... 2) /// cancellable = myRange.publisher /// .assign(to: \.anInt, on: myObject) /// /// // Prints: "anInt was set to: 0; anInt was set to: 1; anInt was set to: 2" /// /// > Important: The ``Subscribers/Assign`` instance created by this operator maintains a strong reference to `object`, and sets it to `nil` when the upstream publisher completes (either normally or with an error). /// /// - Parameters: /// - keyPath: A key path that indicates the property to assign. See [Key-Path Expression](https://developer.apple.com/library/archive/documentation/Swift/Conceptual/Swift_Programming_Language/Expres sions.html#//apple_ref/doc/uid/TP40014097-CH32-ID563) in _The Swift Programming Language_ to learn how to use key paths to specify a property of an object. /// - object: The object that contains The property. The subscriber assigns The object's property every time it receives a new value. /// - Returns: An ``AnyCancellable`` instance. Call ``Cancellable/cancel()`` on this instance when you no longer want the publisher to automatically assign the property. Deinitializing this instance will also cancel automatic assignment. public func assign<Root>(to keyPath: ReferenceWritableKeyPath<Root, Self.Output>, on object: Root) -> AnyCancellableCopy the code
sink(receiveCompletion:@escaping (()->Void),receiveValue:@escaping((output)->Void)) ->AnyCancellable
extension Publisher { /// Attaches a subscriber with closure-based behavior. /// /// Use ``Publisher/sink(receiveCompletion:receiveValue:)`` to observe values received by the publisher and process them using a closure you specify. /// /// In this example, a <doc://com.apple.documentation/documentation/Swift/Range> publisher publishes integers to a ` ` Publisher/sink (receiveCompletion: receiveValue:) ` ` operator 's ` receiveValue ` closure that prints them to the console. Upon completion of the ` ` Publisher/sink (receiveCompletion: receiveValue:) ` ` operator 's ` receiveCompletion ` closure are the successful termination of the stream. /// /// let myRange = (0... 3) /// cancellable = myRange.publisher /// .sink(receiveCompletion: { print ("completion: \($0)") }, /// receiveValue: { print ("value: \($0)") }) /// /// // Prints: /// // value: 0 /// // value: 1 /// // value: 2 /// // value: 3 /// // completion: finished /// /// This method creates the subscriber and immediately requests an unlimited number of values, prior to returning the subscriber. /// The return value should be held, otherwise the stream will be canceled. /// /// - parameter receiveComplete: The closure to execute on completion. /// - parameter receiveValue: The closure to execute on receipt of a value. /// - Returns: A cancellable instance, which you use when you end assignment of the received value. Deallocation of the result will tear down the subscription stream. public func sink(receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void), receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable }Copy the code
Almost every control in SwiftUI can act as a subscriber. The View protocol in SwiftUI defines a.onreceive (publisher) function to use the View as a subscriber
Use the Demo
- Button in the UI
let cancellablePipeline = publishingSource
.receive(on: RunLoop.main)
.assign(to: \.isEnabled, on: yourButton)
cancellablePipeline.cancel()
Copy the code
- Network request
struct MyNetworkingError:Error {
var invalidServerResponse:Int
}
var request = URLRequest(url: regularURL)
request.allowsConstrainedNetworkAccess = false
let network_publisher = URLSession.shared.dataTaskPublisher(for: request)
.tryCatch { error -> URLSession.DataTaskPublisher in
guard error.networkUnavailableReason == .constrained else {
throw error
}
return URLSession.shared.dataTaskPublisher(for: request)
}
.tryMap{ data,response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw MyNetworkingError(invalidServerResponse: 0)
}
return data
}
.eraseToAnyPublisher()
Copy the code
- Example 2
static func randomImage() -> AnyPublisher<RandomImageResponse, GameError> { let url = URL(string: "https://api.unsplash.com/photos/random/?client_id=\(accessToken)")! let config = URLSessionConfiguration.default config.requestCachePolicy = .reloadIgnoringLocalCacheData config.urlCache = nil let session = URLSession(configuration: config) var urlRequest = URLRequest(url: url) urlRequest.addValue("Accept-Version", forHTTPHeaderField: "v1") return session.dataTaskPublisher(for: urlRequest) .tryMap { response -> Data in guard let httpURLResponse = response.response as? HTTPURLResponse, httpURLResponse.statusCode == 200 else { throw GameError.statusCode } return response.data } .decode(type: RandomImageResponse.self, decoder: JSONDecoder()) .mapError { GameError.map($0) } .eraseToAnyPublisher() }Copy the code
- Retry
let remoteDataPublisher = urlSession.dataTaskPublisher(for: self.URL!) .delay(for: DispatchQueue.SchedulerTimeType.Stride(integerLiteral: Int.random(in: 1.. <5)), scheduler: backgroundQueue) .retry(3) .tryMap { data, response -> Data in guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw TestFailureCondition.invalidServerResponse } return data } .decode(type: PostmanEchoTimeStampCheckResponse.self, decoder: JSONDecoder()) .subscribe(on: backgroundQueue) .eraseToAnyPublisher()Copy the code