CocoaPods trunk is moving to be read-only. Read more on the blog, there are 17 months to go.

CombineAsync 2.0.0

CombineAsync 2.0.0

Maintained by Nayanda Haberty.



  • By
  • hainayanda

CombineAsync

CombineAsync is Combine extensions and utilities for an async task

Codacy Badge build test SwiftPM Compatible Version License Platform

Example

To run the example project, clone the repo, and run pod install from the Example directory first.

Requirements

  • Swift 5.5 or higher
  • iOS 13.0 or higher
  • MacOS 10.15 or higher
  • TVOS 13.0 or higher
  • WatchOS 8.0 or higher
  • XCode 13 or higher

Installation

Cocoapods

CombineAsync is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'CombineAsync', '~> 1.2'

Swift Package Manager from XCode

  • Add it using XCode menu File > Swift Package > Add Package Dependency
  • Add https://github.com/hainayanda/CombineAsync.git as Swift Package URL
  • Set rules at version, with Up to Next Major option and put 1.2.0 as its version
  • Click next and wait

Swift Package Manager from Package.swift

Add as your target dependency in Package.swift

dependencies: [
    .package(url: "https://github.com/hainayanda/CombineAsync.git", .upToNextMajor(from: "1.2.0"))
]

Use it in your target as a CombineAsync

 .target(
    name: "MyModule",
    dependencies: ["CombineAsync"]
)

Author

hainayanda, [email protected]

License

CombineAsync is available under the MIT license. See the LICENSE file for more info.

Usage

CombineAsync contains several extensions that can be used when working with Combine and Swift async

Publisher to Async

You can convert any object that implements Publisher into Swift async with a single call:

// implicitly await with 30 second timeout
let result = await publisher.sinkAsynchronously()

// or with timeout explicitly
let timedResult = await publisher.sinkAsynchronously(timeout: 1)

it will produce PublisherToAsyncError if an error happens in the conversion. The errors are:

  • finishedButNoValue
  • timeout
  • failToProduceAnOutput

other than those errors, it will rethrow the error produced by the original Publisher

Sequence of Publisher to an array of output

Similar to Publisher to async, any sequence of Publisher can be converted to async too with a single call:

let results = await arrayOfPublishers.sinkAsynchronously()

// or with timeout
let timedResults = await arrayOfPublishers.sinkAsynchronously(timeout: 1)

Future from async

You can convert Swift async to a Future object with provided convenience init:

let future = Future { 
    try await getSomethingAsync()
}

Publisher Async Sink

Sometimes your sink might execute something asynchronous and you really want to use await inside the sink without explicitly create a Task. This can be achieved by call asyncSink:

publisher.asyncSink { output in
    await somethingAsync(output)
}

Auto Release Sink

You can ignore the cancellable and expect that the closure will be removed on completion by using autoReleaseSink:

publisher.autoReleaseSink { completion in
    // do something on completed
} receiveValue: { output in
    // do something to receive value
}

By default, it will auto release the closure after 30 seconds.

If you want the closure to be released whenever some object is released, just pass the object:

publisher.autoReleaseSink(retainedTo: self) { _ in
    // do something on completed
} receiveValue: { 
    // do something on receive value
}

If you want the closure to be released using a timeout, just pass the timeout:

publisher.autoReleaseSink(timeout: 60) { _ in
    // do something on completed
} receiveValue: { 
    // do something on receive value
}

Whatever you pass, it will try to release the closure whenever one of the conditions is met:

// the closure will be released after completion, or 60 second, or when self is released.
publisher.autoReleaseSink(retainedTo: self, timeout: 60) { _ in
    // do something on completed
} receiveValue: { 
    // do something on receive value
}

If you need to release it manually, the method return RetainStateCancellable object, which is a Cancellable that has a RetainState so you could know whether the closure is already released or not:

// the closure will be released after completion or 30 seconds, or when the self is released.
let retainCancellable = publisher.autoReleaseSink(retainedTo: self, timeout: 30) { _ in
    // do something on completed
} receiveValue: { 
    // do something on receive value
}

let typeErased = retainCancellable.eraseToAnyCancellable()

...
...

switch retainCancellable.state { 
    case .retained: 
    print("closure still retained")
    case .released: 
    print("closure already released")
}

Publisher error recovery

CombineAsync gives you a way to recover from errors using 3 other methods:

// will ignore error and produce AnyPublisher<Output, Never>
publisher.ignoreError()

// will convert the error to output and produce AnyPublisher<Output, Never>
publisher.replaceError { error in convertErrorToOutput(error) }

// will try to convert the error to output and produce AnyPublisher<Output, Failure>
// if the output is nil, it will just pass the error
publisher.replaceErrorIfNeeded { error in convertErrorToOutputIfNeeded(error) }

It's similar to replaceError, but accepts a closure instead of just single output

Sequence of Publisher to a single Publisher

CombineAsync give you a shortcut to merge a sequence of Publisher into a single Publisher that emits an array of output with a single call:

// will collect all the emitted elements from all publishers
let allElementsEmittedPublisher = arrayOfPublishers.merged()

// will collect only the first emitted element from all publishers
let firstElementsEmittedPublisher = arrayOfPublishers.mergedFirsts()

Asynchronous Map

CombineAsync gives you the ability to map using an async mapper. it will run all the mapping parallel and collect the results while maintaining the original order:

// map
let mapped = try await arrayOfID.asyncMap { await getUser(with: $0) }

// compact map
let compactMapped = try await arrayOfID.asyncCompactMap { await getUser(with: $0) }

// with timeout
let timedMapped = try await arrayOfID.asyncMap(timeout: 10) { await getUser(with: $0) }
let timedCompactMapped = try await arrayOfID.asyncCompactMap(timeout: 10) { await getUser(with: $0) }

If you prefer using Publisher instead, change asyncMap to futureMap and asyncCompactMap to futureCompactMap:

// map
let futureMapped: AnyPublisher<[User], Error> = arrayOfID.futureMap { await getUser(with: $0) }

// compact map
let futureCompactMapped: AnyPublisher<[User], Error> = arrayOfID.futureCompactMap { await getUser(with: $0) }

Publisher Async Map

If you need to use Publisher Map asynchronously, you can do it with CombineAsync:

publisher.asyncMap { output in
    await convertOutputAsynchronously(output)
}

There are some async map method you can use:

  • asyncMap which is equivalent with map but asynchronous
  • asyncTryMap which is equivalent with tryMap but asynchronous
  • asyncCompactMap which is equivalent with compactMap but asynchronous
  • asyncTryCompactMap which is equivalent with tryCompactMap but asynchronous

Publisher Map Sequence

If you have the Publisher with Sequence as its Ouput and want to map each element without call map on the sequence output manually, CombineAsync give you the ability to bypass that:

myArrayPublisher.mapSequence { element in
    // do map the element of the sequence to another type
}

Those line of code are equivalent with:

myArrayPublisher.map { output in
    output.map { element in
        // do map the element of the sequence to another type
    }
}

All of the sequence mapper that you can use are:

  • mapSequence(_:) which bypass Sequence.map(_:)
  • compactMapSequence(_:) which bypass Sequence.compactMap(_:)
  • tryMapSequence(_:) which bypass Sequence.map(_:) but with throwing mapper closure
  • tryCompactMapSequence(_:) which bypass Sequence.compactMap(_:) but with throwing mapper closure
  • asyncMapSequence(_:) which bypass Sequence.asyncMap(_:)
  • asyncCompactMapSequence(_:) which bypass Sequence.asyncCompactMap(_:)

Contribute

You know how, just clone and do a pull request