Skip to content
Conbini provides convenience Publishers, operators, and Subscribers to squeeze the most out of Apple’s Combine framework.
Swift
Branch: master
Clone or download
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
Assets Initial Commit Oct 24, 2019
Sources DeferredFuture implemented & tested Nov 15, 2019
Tests DeferredFuture implemented & tested Nov 15, 2019
.gitignore
LICENSE Initial Commit Oct 24, 2019
Package.swift Generic play Nov 2, 2019
README.md

README.md

Conbini icon

Conbini provides convenience Publishers, operators, and Subscribers to squeeze the most out of Apple's Combine framework.

Swift 5.1 platforms License

Operators

  • then ignores all values and executes the provided publisher once a successful completion is received. If a failed completion is emitted, it is forwarded downstream.

    let publisher = setConfigurationOnServer.then {
        subscribeToWebsocket.publisher
    }
  • result subscribes to the receiving publisher and executes the provided closure when a single value followed by a successful completion is received. In case of failure, the handler is executed with such failure.

    let cancellable = serverRequest.result { (result) in
        switch result {
        case .success(let value): ...
        case .failure(let error): ...
        }
    }
  • asyncMap transforms elements received from upstream (similar to map), but the result is returned in a promise instead of using the return statement (similar to Future). Useful when asynchronous operations must be performed sequentially on a value.

    let publisher = [1, 2].publisher.asyncMap { (value, promise) in
        queue.async {
            let newValue = String(value * 10)
            promise(newValue)
        }
    }

    This operator also provides a try variant accepting a result (instead of a value).

    let publisher = [1, 2].publisher.asyncTryMap { (value, promise) in
        do {
            let newValue = try someOperation()
            promise(.success(newValue))
        } catch let error {
            promise(.failure(error))
        }
    }
  • sequentialMap transform elements received from upstream (as asyncMap) with the twist that it allows you to call multiple times the promise callback; effectively transforming one value into many results.

    let publisher = [1, 2].publisher.sequentialMap { (value, promise) in
        queue.async {
            promise(value * 10 + 1, .continue)
            promise(value * 10 + 2, .continue)
            promise(value * 10 + 3, .finished)
        }
    }
    
    // Downstream will receive: [11, 12, 13, 21, 22, 23]

    The SequentialMap publisher executes one upstream value at a time. It doesn't request or fetch a previously sent upstream value till the transform closure is fully done and promise(..., .finished) has been called.

    This operator also provides a try variant accepting a result (instead of a value).

    let publisher = [1, 2].publisher.sequentialTryMap { (value, promise) in
        queue.async {
            promise( .success(value * 10 + 1), .continue)
            promise( .success(value * 10 + 2), .continue)
            promise( .failure(CustomError), .finished)
        }
    }
  • sequentialFlatMap performs a similar operation to flatMap (i.e. flattens/executes a publisher emitted from upstream); but instead of accepting willy-nilly all emitted publishers, it only requests one value at a time (through backpressure mechanisms). Useful for operations/enpoints that must be performed sequentially.

    [enpointA, endpointB, endpointC].publisher
        .sequentialFlatMap { $0 }
    // Downstream will receive: [resultEndpointA, resultEndpointB, resultEndpointC]

    This publisher works "as expected" even with upstream publishers that disregard backpressure (e.g. PassthroughSubject). It buffers values internally and execute the generated publisher depending on the subscriber's demand and whether a publisher is currently in operation. Do note, that if a failure completion is received, the whole publisher will finish and any publisher being buffered won't have a chance to execute. This is a similar behavior as Combine's buffer() operator.

Publishers

  • Deferred... publishers accept a closure that is executed once a greater-than-zero demand is requested. They have several flavors:

    • DeferredValue emits a single value and then completes. The value is not provided/cached, but instead a closure will generate it. The closure is executed once a positive subscription is received.

      let publisher = DeferredValue<Int,CustomError> {
          return intenseProcessing()
      }

      A Try variant is also offered, enabling you to throw from within the closure. It loses the concrete error type (i.e. it gets converted to Swift.Error).

      let publisher = DeferredTryValue {
          return try intenseProcessing()
      }
    • DeferredResult offers the same functionality as DeferredValue, but the closure generates a Result instead.

      let publisher = DeferredResult {
        guard someExpression else { return .failure(CustomError()) }
        return .success(someValue)
      }
    • DeferredCompletion offers the same functionality as DeferredValue, but the closure only generates a completion event.

      let publisher = DeferredCompletion {
          return errorOrNil
      }

      A Try variant is also offered, enabling you to throw from within the closure; but it loses the concrete error type (i.e. gets converted to Swift.Error).

      let publisher = DeferredTryCompletion {
          try somethingThatMighFail()
      }
    • DeferredPassthrough is similar to wrapping a Passthrough subject on a Deferred closure, with the diferrence that the Passthrough given on the closure is already wired on the publisher chain and can start sending values right away. Also, the memory management is taken care of and every new subscriber receives a new subject (closure re-execution).

      let publisher = DeferredPassthrough { (subject) in
        subject.send(something)
        subject.send(somethingElse)
        subject.send(completion: .finished)
      }

    There are several reason for these publishers to exist instead of using other Combine-provided closure such as Just, Future, or Deferred:

    • Future publishers execute their provided closure right away (upon initialization) and then cache the returned value. That value is then forwarded for any future subscription. Deferred... closures await for subscriptions and a greater-than-zero demand before executing. This also means, the closure will re-execute for any new subscriber.
    • Deferred is the most similar in functionality, but it only accepts a publisher.
  • Then provides the functionality of the then operator.

Testing

Conbini provides convenience subscribers to ease code testing. These subscribers make the test wait till a specific expectation is fulfilled (or making the test fail in a negative case). Furthermore, if a timeout ellapses or a expectation is not fulfilled, the affected test line will be marked in red correctly in Xcode.

  • expectsCompletion subscribes to a publisher making the running test wait for a successful completion while ignoring all emitted values.

    publisherChain.expectsCompletion(timeout: 0.8, on: test)
  • expectsFailure subscribes to a publisher making the running test wait for a failed completion while ignoring all emitted values.

    publisherChain.expectsFailure(timeout: 0.8, on: test)
  • expectsOne subscribes to a publisher making the running test wait for a single value and a successful completion. If more than one values are emitted or the publisher fails, the subscription gets cancelled and the test fails.

    let emittedValue = publisherChain.expectsOne(timeout: 0.8, on: test)
  • expectsAll subscribes to a publisher making the running test wait for zero or more values and a successful completion.

    let emittedValues = publisherChain.expectsAll(timeout: 0.8, on: test)
  • expectsAtLeast subscribes to a publisher making the running test wait for at least the provided amount of values. Once the provided amount of values is received, the publisher gets cancelled and the values are returned.

    let emittedValues = publisherChain.expectsAtLeast(values: 5, timeout: 0.8, on: test)

    This operator/subscriber accepts an optional closure to check every value received.

    let emittedValues = publisherChain.expectsAtLeast(values: 5, timeout: 0.8, on: test) { (value) in
      XCTAssert...
    }

Quirks

The testing conveniences depend on XCTest, which is not available on regular execution. That is why Conbini is offered in two flavors:

  • import Conbini includes all code excepts the testing conveniences.
  • import ConbiniForTesting includes everything.

The rule of thumb is to use import Conbini in your regular code (e.g. within your framework or app) and write import ConbiniForTesting within your test target files.

References

The framework name references both the Combine framework and the helpful Japanese convenience stores 😄

You can’t perform that action at this time.