A small set of classes and functions to make easy use of Futures, Promises and async computation in general. All written in Swift for iOS 8+, WatchOS 2, tvOS and Mac OS X apps.
Switch branches/tags
Clone or download
whoover Merge pull request #21 from spring-media/swift_4.2
Migrate to Swift 4.2 and XCode 10.
Latest commit de0a7ba Nov 15, 2018

README.md

Pied Piper

Build Status Version Carthage compatible License Platform

A small set of classes and functions to make easy use of Futures, Promises and async computation in general. All written in Swift for iOS 8+, WatchOS 2, tvOS and Mac OS X apps.

Contents of this Readme

What is Pied Piper?

Pied Piper is a small set of classes, functions and convenience operators to write easy asynchronous code in your application.

With Pied Piper you can:

Installation

CocoaPods

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

pod "PiedPiper"

Submodule

If you don't use CocoaPods, you can still add Pied Piper as a submodule, drag and drop PiedPiperSample.xcodeproj into your project, and embed PiedPiper.framework in your target.

  • Drag PiedPiperSample.xcodeproj to your project
  • Select your app target
  • Click the + button on the Embedded binaries section
  • Add PiedPiper.framework

Carthage

Carthage is also supported.

Manual

You can directly drag and drop the needed files into your project, but keep in mind that this way you won't be able to automatically get all the latest Pied Piper features (e.g. new files including new operations).

The files are contained in the PiedPiper folder and work for the iOS, watchOS, MacOS and tvOS frameworks.

Playground

We ship a small Xcode Playground with the project, so you can quickly see how Pied Piper works and experiment with your custom layers, layers combinations and different configurations for requests pooling, capping, etc.

To use our Playground, please follow these steps:

  • Open the Xcode project PiedPiperSample.xcodeproj
  • Select the Pied Piper framework target, and a 64-bit platform (e.g. iPhone 6)
  • Build the target with ⌘+B
  • Click the Playground file PiedPiper.playground
  • Write your code

Requirements

  • iOS 8.0+
  • WatchOS 2+
  • Mac OS X 10.9+
  • Xcode 7.3+
  • tvOS 9+

Usage

To run the example project, clone the repo.

Usage examples

Futures

A Future is an object representing a computation that may not have happened yet. You can add callback handlers for the success, failure, and cancelation events of a specific Future.

Please keep in mind that a Future is not a signal. It can only fail, succeed or cancel (mutually exclusive) and it can only do so once.

Please also note that a Future is "read-only". This means you can not actively determine the result of an instance. If you are implementing your own asynchronous computations, please have a look at promises instead.

// The login function returns a Future
let login = userManager.login(username, password)

// You can specify success or failure callbacks, and chain the calls together
login.onSuccess { user in
  print("User \(user.username) logged in successfully!")
}.onFailure { error in
  print("Error \(error) during login")
}.onCancel {
  print("Login was cancelled by the user")
}

// Or you can use onCompletion instead
login.onCompletion { result in
  switch result {
  case .Success(let user):
  	print("User \(user.username) logged in successfully!")
  case .Error(let error):
    print("Error \(error) during login")
  case .Cancelled:
    print("Login was cancelled by the user")
  }
}

Since Pied Piper 0.8 you can use convenience initializers on Future if you already know the result without any asynchronous work:

let future = Future(10)

// or

let future = Future(MyError.SomeError)

// or

let future = Future(value: possiblyNil, error: MyError.SomeError)

// or

let future: Future<UIImage> = Future {
	return asyncCodeThatFetchesImage()
}

Promises

Futures are really handy when you are the user of some async computations. But sometimes you may want to be the producer of these, and in this case you need to be able to determine when a Future should succeed or fail. Then you need a Promise.

func login(username: String, password: String) -> Future<User> {
  // Promises, like Futures, are generic
  let promise = Promise<User>()

  GCD.background {
    //Request to the server...
    //...
    if response.statusCode == 200 {
      // Success
      promise.succeed(userFromResponse(response))
    } else {
      // Your ErrorType can be used as an argument to fail
      promise.fail(LoginError.InvalidCredentials)
    }
  }

  // If the user wants to cancel the login request
  promise.onCancel {
    //cancel the request to the server
    //...
  }

  // we don't want users to be able to fail our request
  return promise.future
}

GCD computation

Pied Piper offers some helper functions on top of GCD to run blocks of code on the main queue or on a background queue.

//Without Pied Piper
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
  // Execute your asynchronous code
  // ...
  let result = 10

  // Notify on the main queue
  dispatch_async(dispatch_get_main_queue()) {
    print("Result is \(result)")
  }
}

//With Pied Piper
GCD.background { Void -> Int in
  // Execute your asynchronous code
  return 10
}.main { result in
  print("Result is \(result)")
}

You can also run your asynchronous code on serial queues or your own queues.

// Serial queue
let queue = GCD.serial("test")

// Your own queue
let queue = GCD(queue: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0))

// Then...
queue.async { Void -> Int in
  return 10
}.main {
  print($0)
}

Advanced usage with Futures

Since Pied Piper 0.8 many convenience functions are available on Future values, like map, flatMap, filter, recover, retry, zip, reduce, mergeSome and mergeAll. Moreover, traverse is available for all SequenceType values.

Since Pied Piper 0.9 some more functions are available like snooze, timeout and firstCompleted (the latter for a SequenceType of Future values).

Keep in mind that some of these functions (map, flatMap and filter) are also available on Result values. They work just like their Future counterparts.

FlatMap, Map, Filter

// In this snippet `doStuff` returns a Future<Int>
let newFuture = doStuff().filter { value in
  value > 0
}.flatMap { value in
  Future("\(value)")
}.map { value in
  "The result is \(value)"
}

// `newFuture` is now a Future<String> that will only succeed when the original Future succeeds with a value > 0

Recover

It's now also possible to provide a "catch-all" handler to recover a failing Future:

let numberOfItemsTask: Future<Int> = doLongRunningTask()
  .flatMap { result in
    result.processAndPersist()
  }.map { result in
    result.numberOfItems
  }.recover {
    cache.lastNumberOfItems
  }

numberOfItemsTask.onSuccess { numberOfItems in
  // This will be called even if one of the previous operations fails, with the rescue value `cache.lastNumberOfItems`
  // ...
}

Zip

// Example for zip

let first: Future<Int> = doFoo()
let second: Future<String> = doBar()

let zipped = first.zip(second).onSuccess { (anInteger, aString) in
  // you get an Int and a String here
}

// or:

let first: Future<Int> = doFoo()
let second: Result<String> = doBar()

let zipped = first.zip(second).onSuccess { (anInteger, aString) in
  // you get an Int and a String here
}

Reduce

// Let's assume this value contains a list of server requests where each request obtains the number of items in a given category
let serverRequests: [Future<Int>] = doFoo()

// With this `reduce` call we calculate the total number of items
let sumOfServerResults = serverRequests.reduce(0, combine: +).onSuccess {
  // We get here only if all futures succeed
  print("Sum of results is \($0)")
}

MergeAll

// Let's assume this value contains a list of server requests where each request obtains the number of items in a given category
let serverRequests: [Future<Int>] = doFoo()

// With this `mergeAll` call we collapse the requests into one containing the result of all of them, if they all succeeded, or none if one fails
let allServerResults = serverRequests.mergeAll().onSuccess { results in
  // We get here only if all futures succeed
  // `results` is an [Int]
}

All

all behaves exactly like mergeAll, except that it doesn't bring the success values with it.

// Let's assume this value contains a list of server requests where each request obtains the number of items in a given category
let serverRequests: [Future<Int>] = doFoo()

// With this `all` call we collapse the requests into one that will succeed if all of the elements succeed, otherwise it will fail
let allServerResults = serverRequests.mergeAll().onSuccess {
  // We get here only if all futures succeed
}

MergeSome

// Let's assume this value contains a list of server requests where each request obtains the number of items in a given category
let serverRequests: [Future<Int>] = doFoo()

// With this `mergeSome` call we collapse the requests into one containing the result of just the ones that succeed
let allServerResults = serverRequests.mergeSome().onSuccess { results in
  // We get here and results.count == the number of succeeded requests
  // `results` is an [Int]
}

// Note: `merge` succeeds only when _all_ requests succeed, while `mergeSome` always succeeds and filters out the failed requests from the results

FirstCompleted

// Let's assume this value contains a list of server requests where each request comes from a different server but all of them answer the same question
let serverRequests: [Future<[Product]>] = gatherRequests()

/// With this `firstCompleted` call we basically declare we are interested in only the first result and want to discard the remaining ones
let firstResult = serverRequests.firstCompleted().onSuccess { products in
  // We get here with the first completing request
}

Traverse

// Let's assume this list contains some product identifiers
let productIdentifiers: [Int] = basketProductsIds()

// With this `traverse` call we create a Future for every identifier (for instance to retrieve details of each product), and we merge the results into one final Future
let allProductDetails = productIdentifiers.traverse({ productId in
  // Let's assume this call returns a Future<Product>
  ProductManager.retrieveDetailsForProduct(productId)
}).onSuccess { products in
  // We get here only if all futures succeed
  // `products` is a [Product]
}

Snooze

// Sometimes we may be running multiple operations in parallel and we may want to have some time in between to gather multiple values
let firstOperation = doFoo()
let secondOperation = doBar()

// With this call to `snooze` we declare we're not interested in immediate feedback from the second operation because we may want to process the result of the first, first.
secondOperation.snooze(0.5).onSuccess { value in
}

Timeout

// Sometimes we may want to set an upper bound to the time an operation can run before moving on or showing something to the user
let longRunningOperation = doFoo()

longRunningOperation.timeout(after: 5).onFailure { err in
  if let error = err as? FutureError where error = FutureError.Timeout {
    // The operation timed out, but of course it's still running. We may keep adding observers to the original variable if we are still interested in the final result (see next lines)
    showAlert()
  }
}

longRunningOperation.onSuccess { value in
  showSuccessDialog()
}

Retry

// Sometimes we want to retry a given block of code for a certain number of times before failing
retry(3, every: 0.5) {
  return networkManager.fetchLatestMessages() // This returns a Future
}.onSuccess { messages in
  // The operation succeeded at least once
}.onFailure { _ in
  // The operation failed 4 times (1 + retry count of 3)
}

Function composition

Pied Piper can also be helpful when you want to compose the result of asynchronous computation in a single function or object.

There are 3 public functions as of Pied Piper 0.7:

  • Compose functions
func randomInt() -> Int {
  return 4 //Guaranteed random
}

func stringifyInt(number: Int) -> String {
  return "\(number)"
}

func helloString(input: String) -> String {
  return "Hello \(input)!"
}

let composition = randomInt >>> stringifyInt >>> helloString

composition() //Prints "Hello 4!"

If one of the functions returns an Optional, and at call time the value is nil, the computation stops there:

func randomInt() -> Int {
  return 4 //Guaranteed random
}

func stringifyInt(number: Int) -> String? {
  return nil
}

func helloString(input: String) -> String {
  return "Hello \(input)"
}

let composition = randomInt >>> stringifyInt >>> helloString

composition() //Doesn't print
  • Compose Futures:
func intFuture(input: Int) -> Future<Int> {
  return Future(input)
}

func stringFuture(input: Int) -> Future<String> {
  return Future("Hello \(input)!")
}

let composition = intFuture >>> stringFuture

composition(1).onSuccess { result in
  print(result) //Prints "Hello 1!"
}

Tests

Pied Piper is thouroughly tested so that the features it's designed to provide are safe for refactoring and as bug-free as possible.

We use Quick and Nimble instead of XCTest in order to have a good BDD test layout.

As of today, there are around 600 tests for Pied Piper (see the folder PiedPiperTests).

Future development

Pied Piper is under development and here you can see all the open issues. They are assigned to milestones so that you can have an idea of when a given feature will be shipped.

If you want to contribute to this repo, please:

  • Create an issue explaining your problem and your solution
  • Clone the repo on your local machine
  • Create a branch with the issue number and a short abstract of the feature name
  • Implement your solution
  • Write tests (untested features won't be merged)
  • When all the tests are written and green, create a pull request, with a short description of the approach taken

Apps using Pied Piper

Using Pied Piper? Please let us know through a Pull request, we'll be happy to mention your app!

Authors

Pied Piper was made in-house by WeltN24

Contributors:

Vittorio Monaco, vittorio.monaco@weltn24.de, @vittoriom on Github, @Vittorio_Monaco on Twitter

License

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

Acknowledgements

Pied Piper internally uses:

  • Some parts of ReadWriteLock.swift (in particular the pthread-based read-write lock) belonging to Deferred (available on Github)