PiedPiper 0.10.1

PiedPiper 0.10.1

测试已测试
Lang语言 SwiftSwift
许可证 MIT
发布上次发布2016年11月
SPM支持 SPM

Vittorio Monaco 维护。



PiedPiper 0.10.1

  • Vittorio Monaco

Pied Piper

Build Status

一组便于在 FuturePromise 和异步计算方面使用的类和函数。所有代码均用 Swift 编写,适用于 iOS 8+watchOS 2tvOSMac OS X 应用。

本 Readme 内容

什么是 Pied Piper?

Pied Piper 是一组类、函数和方便的运算符,可用于在您的应用程序中 编写易于异步代码

使用 Pied Piper,您可以

安装

子模块

如果您不使用 CocoaPods,您仍然可以将 Pied Piper 作为子模块添加,将 PiedPiperSample.xcodeproj 拖放到您的项目中,并将 PiedPiper.framework 嵌入您的目标中。

  • PiedPiperSample.xcodeproj 拖放到您的项目中
  • 选择您的应用目标
  • 嵌入式可执行文件 部分的 + 按钮上单击
  • 添加 PiedPiper.framework

手动

您可以直接将所需的文件拖放到项目中,但请注意,这种方式您将无法自动获取所有最新的 Pied Piper 功能(例如,包括新操作的新文件)。

这些文件包含在 PiedPiper 文件夹中,并对 iOSwatchOSMacOStvOS 框架有效。

游乐场

项目包含一个小型的Xcode游乐场,让您快速了解Pied Piper的工作原理,并试验自定义层级、层级组合以及针对请求池、配额等的不同配置。

要使用我们的游乐场,请按照以下步骤操作:

  • 打开Xcode项目PiedPiperSample.xcodeproj
  • 选择Pied Piper框架目标,并选择一个64位平台(例如:iPhone 6
  • ⌘+B构建目标
  • 点击游乐场文件PiedPiper.playground
  • 编写你的代码

要求

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

使用

要运行示例项目,请克隆仓库。

使用示例

未来(Futures)

Future是一个表示尚未发生的计算的物体。您可以添加回调处理程序以处理特定Future的成功、失败和取消事件。

请注意,Future不是一个信号。它只能失败、成功或取消(互斥操作),并且只能进行一次。

另外,请注意Future是“只读的”。这意味着您不能积极地确定实例的结果。如果您正在实现自己的异步计算,请参阅许诺(Promises)

// 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")
  }
}

Pied Piper 0.8起,如果您已经知道结果而没有进行任何异步操作,您可以使用Future上的便利初始化器。

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)

Future在您使用一些异步计算时非常方便。但有时您可能想是这些计算的提供者,在这种情况下,您需要能够确定Future何时应该成功或失败。那么您就需要一个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
}

Grand Central Dispatch(GCD)计算

Pied Piper在GCD之上提供了几个辅助函数,可以运行代码块在主线程或后台线程。

//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)")
}

您还可以在顺序队列或您自己的队列上运行异步代码。

// 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)
}

使用Future的高级用法

Pied Piper 0.8以来,在Future值上提供了许多便利函数,如mapflatMapfilterrecoverretryzipreducemergeSomemergeAll。此外,对于所有SequenceType值,还提供了traverse

Pied Piper 0.9起,还提供了一些额外的函数,如snoozetimeoutfirstCompleted(后者的SequenceTypeFuture值)。

请注意,其中一些函数(如mapflatMapfilter)也适用于Result值。它们的工作方式与它们的Future对应物一样。

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)

现在也可以提供“捕获所有异常”的处理程序以恢复失败的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 的行为与 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 `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)
}

函数组合

Pied Piper 当您希望在单个函数或对象中组合异步计算结果时也非常有用。

截至 Pied Piper 0.7,有 3 个公开函数

  • 组合函数
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!"

如果有其中一个函数返回一个 Optional,并且在调用时间该值是 nil,计算将在这里停止

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
  • 组合 Future
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!"
}

测试

Pied Piper 已经过彻底测试,因此它设计的功能是安全的用于重构,并且尽可能无错误。

我们使用 QuickNimble 而不是 XCTest,以便具有良好的行为驱动测试布局。

截至今天,Pied Piper 约有 600 个测试(查看 PiedPiperTests 文件夹)。

未来的开发

Pied Piper 正在开发中,您可以通过 此处 看到所有未解决的问题。它们被分配到了里程碑,这样您就可以了解某个特定功能何时发布。

如果您想为这个仓库做出贡献,请

  • 创建一个问题描述您的问题和解决方案的问题
  • 在您的本地计算机上克隆仓库
  • 根据问题编号创建一个分支,并简要说明特性名称
  • 实现您的解决方案
  • 编写测试(未测试的特性不会合并)
  • 当所有测试都编写并绿色时,创建一个拉取请求,并简要描述所用方法

使用 Pied Piper 的应用

如果使用 Pied Piper,请通过拉取请求告诉我们,我们将很乐意提到您的应用!

作者

Pied Piper 由 WeltN24 内部创建。

贡献者

Vittorio Monaco,[email protected],在 Github 上的@vittoriom,Twitter 上的@Vittorio_Monaco

许可证

Pied Piper 可在 MIT 许可证下使用。有关更多信息,请参阅许可证文件。

致谢

内部使用

  • ReadWriteLock.swift的一些部分(特别是基于pthread的读写锁),属于Deferred(可在Github上获取)。