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 controls what and when 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

  1. Button in the UI
let cancellablePipeline = publishingSource 
                         .receive(on: RunLoop.main) 
                         .assign(to: \.isEnabled, on: yourButton) 
cancellablePipeline.cancel() 
Copy the code
  1. 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
  1. 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