PiuPiu 1.4.6

PiuPiu 1.4.6

Jacob Sikorski 维护。



PiuPiu 1.4.6

  • By
  • Jacob Sikorski

Swift 5 iOS 9+ Carthage CocoaPods GitHub Build

PiuPiu

之前被称为 NetworkKit,该项目的名称已被更改为支持 CocoaPods。PiuPiu 为 iOS 增加了 Future(也称为:Promise)的概念。其目的是使网络调用更加简洁和简单,并为开发者提供比其他任何网络框架更多的可定制性。

问题:为什么我应该使用这个框架?答案:因为,你喜欢干净代码。

问题:为什么这个名字这么傻?答案:因为“piu piu”是激光的声音。而激光来自未来。

问题:哪种熊最好?答案:错误!黑熊!

更新

1.4.0

  • Request 协议更改为返回 URLRequest
  • RequestSerializer 替换 DispatcherNetworkDispatcher
  • 回调只会触发一次。一旦触发回调,其引用就会被释放(置为 null)。
    • 这是为了防止内存泄漏。
  • 添加了使用基本 URLRequestDataDispatcherUploadDispatcherDownloadDispatcher 协议。
    • 添加了实现所有 3 个协议的 URLRequestDispatcher 类。
    • 包括 MockURLRequestDispatcher在内的分配器上的回调都是弱引用。现在您必须拥有对分配器的引用。
    • 在分配器被释放时取消请求。
  • ResponseFuture 添加了 cancellation 回调。
    • 这可以通过使用 cancel 或在任何 join(序列仅)、thenreplaceaction(初始化)回调中返回 nil 来手动触发。
    • 此回调不会取消实际请求,只是停止在最后的 cancellationcompletion 回调之后继续执行 ResponseFuture 的任何进一步执行。
  • 添加了不传递响应对象的并行 join 回调。此回调是非逃逸的。
  • 稍微更好的多线程支持。
    • 默认情况下,then 在后台线程上触发。
    • successresponseerrorcompletioncancellation回调函数始终在主线程上同步。
  • 通过progress回调添加进度更新。
  • 通过MockURLRequestDispatcher添加更好的请求模拟工具。

1.3.0

  • PewPew重命名为PiuPiu
    • 为了处理这次迁移,将所有import PewPew替换为import PiuPiu
  • 修复了Carthage的构建问题。
  • 删除了不必要的文件。

1.2.0

  • URLRequestProvider现在返回一个可选的URL。这将安全地处理无效的URL,而不是强制开发者使用感叹号(!)。
  • 向BasicRequest添加了JSON数组序列化方法。

1.1.0

移除了默认翻译。

1.0.1

修复了由于项目重命名导致翻译时发生的崩溃。

特性

  • 网络请求包装器
  • 使用Futures(即Promises)允许可伸缩性和干燥性
  • 处理可解码和JSON的便利方法
  • 易于集成
  • 处理常见的HTTP错误
  • 强类型和安全的解包响应
  • 简洁明了!

安装

Carthage

Carthage是一个去中心化的依赖管理器,它会构建你的依赖关系并为你提供二进制框架。

您可以使用以下命令使用Homebrew安装Carthage

$ brew update
$ brew install carthage

要使用Carthage将PiuPiu集成到您的Xcode项目中,请在您的Cartfile中指定它。

github "cuba/PiuPiu" ~> 1.4

运行carthage update构建框架,并将构建的PiuPiu.framework拖放到您的Xcode项目中。

用法

1. 将 PiuPiu 导入您的文件

import PiuPiu

2. 创建一个 Dispatcher 实例

所有请求都通过一个 dispatcher 来完成。Dispatcher 有三种协议:

  • DataDispatcher:执行标准 HTTP 请求并返回一个包含 Response<Data?> 对象的 ResponseFuture。也可以用来上传数据。
  • DownloadDispatcher:用于下载数据。它返回一个只包含 Data 对象的 ResponseFuture
  • UploadDispatcher:用于上传数据。通常可以用 DataDispatcher 来代替,但它提供了一些上传特定的优点,如更好的进度更新。

为了方便,提供了一个实现了所有三种协议的 URLRequestDispatcher

class ViewController: UIViewController {
    private let dispatcher = URLRequestDispatcher()
    
    // ... 
}

您应该保留对这个对象的强引用,因为它被回调以弱引用持有。

3. 发送请求。

let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
let request = URLRequest(url: url, method: .get)

dispatcher.dataFuture(from: request).response({ response in
    // Handles any responses including negative responses such as 4xx and 5xx

    // The error object is available if we get an
    // undesirable status code such as a 4xx or 5xx
    if let error = response.error {
        // Throwing an error in any callback will trigger the `error` callback.
        // This allows us to pool all failures in one place.
        throw error
    }

    let post = try response.decode(Post.self)
    // Do something with our deserialized object
    // ...
    print(post)
}).error({ error in
    // Handles any errors during the request process,
    // including all request creation errors and anything
    // thrown in the `then` or `success` callbacks.
}).completion({
    // The completion callback is guaranteed to be called once
    // for every time the `start` method is triggered on the future.
}).send()

注意:如果您不调用 start(),将不会发生任何操作。

4. (可选)分离关注点并转换未来

该笑话不是故意的(真的)

现在让我们将解码对象的未来部分移动到另一个方法中。这样,我们的业务逻辑就不会与我们的序列化逻辑混淆。使用 futures 的一个好处是我们可以返回它们!

让我们创建一个类似以下的方法

private func getPost(id: Int) -> ResponseFuture<Post> {
    // We create a future and tell it to transform the response using the
    // `then` callback. After this we can return this future so the callbacks will
    // be triggered using the transformed object. We may re-use this method in different
    return dispatcher.dataFuture(from: {
        let url = URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!
        return URLRequest(url: url, method: .get)
    }).then({ response -> Post in
        if let error = response.error {
            // The error is available when a non-2xx response comes in
            // Such as a 4xx or 5xx
            // You may also parse a custom error object here.
            throw error
        } else {
            // Return the decoded object. If an error is thrown while decoding,
            // It will be caught in the `error` callback.
            return try response.decode(Post.self)
        }
    })
}

注意:在这种情况下,我们故意没有调用 start()

然后我们可以这样简单地调用它

getPost(id: 1).response({ post in
    // Handle the success which will give your posts.
    responseExpectation.fulfill()
}).error({ error in
    // Triggers whenever an error is thrown.
    // This includes deserialization errors, unwraping failures, and anything else that is thrown
    // in a any other throwable callback.
}).completion({
    // Always triggered at the very end to inform you this future has been satisfied.
}).send()

未来

您已经看到 ResponseFuture 允许您链式调用回调、转换响应对象并在之间传递。但除了上面简单的例子,您还可以做很多其他事情来使您的代码更加简洁!

dispatcher.dataFuture(from: {
    let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
    return URLRequest(url: url, method: .get)
}).then({ response -> Post in
    // Handles any responses and transforms them to another type
    // This includes negative responses such as 4xx and 5xx

    // The error object is available if we get an
    // undesirable status code such as a 4xx or 5xx
    if let error = response.error {
        // Throwing an error in any callback will trigger the `error` callback.
        // This allows us to pool all the errors in one place.
        throw error
    }
    
    return try response.decode(Post.self)
}).replace({ post -> ResponseFuture<EnrichedPost> in
    // Perform some operation that itself uses a future
    // such as something heavy like markdown parsing.
    // Any callback can be transformed to a future.
    return self.enrich(post: post)
}).join({ enrichedPost -> ResponseFuture<User> in
    // Joins a future with another one returning both results
    return self.fetchUser(forId: post.userId)
}).response({ enrichedPost, user in
    // The final response callback includes all the transformations and
    // Joins we had previously performed.
}).error({ error in
    // Handles any errors throw in any callbacks
}).completion({
    // At the end of all the callbacks, this is triggered once. Error or no error.
}).send()

回调

responsesuccess 回调

response 回调在请求接收且链式回调(如 thenjoin)中没有抛出错误时触发。在回调序列的末尾,这给您提供了您希望 "承诺" 返回的精确内容。

dispatcher.dataFuture(from: request).response({ response in
    // Triggered when a response is recieved and all callbacks succeed.
})

注意:此方法应 调用 一次

error 回调

将此视为 do 块上的 catch。从触发 send() 的那一刻起,每当在回调序列中抛出错误时,都会触发错误回调。这包括抛出在任何其他回调中的错误。

dispatcher.dataFuture(from: request).error({ error in
    // Any errors thrown in any other callback will be triggered here.
    // Think of this as the `catch` on a `do` block.
})

注意:此方法应 调用 一次

completion 回调

完成回调总是紧接在每次触发 send()start() 后,在所有 ResponseFuture 回调完成之后触发。

dispatcher.dataFuture(from: request).completion({
    // The completion callback guaranteed to be called once
    // for every time the `send` or `start` method is triggered on the callback.
})

注意:此方法应 调用 一次

then 回调

此回调将 response 类型转换为其他类型。此操作在后台队列上执行,因此重操作不会锁定主队列。

警告:您应避免在上此回调中使用 self 。仅用于转换未来。

dispatcher.dataFuture(from: request).then({ response -> Post in
    // The `then` callback transforms a successful response to another object
    // You can return any object here and this will be reflected on the `success` callback.
    return try response.decode(Post.self)
}).response({ post in
    // Handles any success responses.
    // In this case the object returned in the `then` method.
})

replace 回调

此回调通过使用另一个回调将未来转换为另一种类型。这允许我们在回调内部进行异步调用。

dispatcher.dataFuture(from: request).then({ response -> Post in
    return try response.decode(Post.self)
}).replace({ [weak self] post -> ResponseFuture<EnrichedPost> in
    // Perform some operation operation that itself requires a future
    // such as something heavy like markdown parsing.
    return self?.enrich(post: post)
}).response({ enrichedPost in
    // The final response callback has the enriched post.
})

注意:您可以通过返回 nil 来停止请求过程。当您希望有一个弱引用的 self 时非常有用。

join 回调

此回调将未来转换为另一种类型,其中包含其原始结果以及返回回调的结果。此回调有两种风味:并行和系列。

系列 join

串行连接等待第一个响应并将其传递给回调,以便您可以根据那个响应进行请求。

dispatcher.dataFuture(from: {
    let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
    return URLRequest(url: url, method: .get)
}).then({ response in
    // Transform this response so that we can reference it in the join callback.
    return try response.decode(Post.self)
}).join({ [weak self] post -> ResponseFuture<User>? in
    guard let self = self else {
        // We used [weak self] because our dispatcher is referenced on self.
        // Returning nil will cancel execution of this promise
        // and triger the `cancellation` and `completion` callbacks.
        // Do this check to prevent memory leaks.
        return nil
    }

    // Joins a future with another one returning both results.
    // The post is passed so it can be used in the second request.
    // In this case, we take the user ID of the post to construct our URL.
    let url = URL(string: "https://jsonplaceholder.typicode.com/users/\(post.userId)")!
    let request = URLRequest(url: url, method: .get)

    return self.dispatcher.dataFuture(from: request).then({ response -> User in
        return try response.decode(User.self)
    })
}).success({ post, user in
    // The final response callback includes both results.
    expectation.fulfill()
}).send()

注意:您可以通过返回 nil 来停止请求过程。当您希望有一个弱引用的 self 时非常有用。

并行 join 回调

此回调不会等待原始请求完成,并立即执行。这对于系列调用非常有用。

dispatcher.dataFuture(from: {
    let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
    return URLRequest(url: url, method: .get)
}).then({ response in
    return try response.decode([Post].self)
}).join({ () -> ResponseFuture<[User]> in
    // Joins a future with another one returning both results.
    // Since this callback is non-escaping, you don't have to use [weak self]
    let url = URL(string: "https://jsonplaceholder.typicode.com/users")!
    let request = URLRequest(url: url, method: .get)

    return self.dispatcher.dataFuture(from: request).then({ response -> [User] in
        return try response.decode([User].self)
    })
}).success({ posts, users in
    // The final response callback includes both results.
    expectation.fulfill()
}).send()

注意:此回调将立即执行(它不是逃逸的)。因此,不需要 [weak self]

sendstart

这将为 ResponseFuture 启动。换句话说,将触发 action 回调并将请求发送到服务器。

注意:如果未调用此方法,则不会发生任何事情(不会进行请求)。注意:应在声明所有回调(successfailureerrorthen 等等)之后调用此方法。

创建自己的 ResponseFuture

出于各种原因,您可以创建自己的 ResponseFuture。这可以在另一个 future 的 joinreplace 回调中使用,以便进行一些链式操作。

以下是一个示例,展示了一个在另一个线程中进行开销较大的操作的响应 future。

return ResponseFuture<UIImage>(action: { future in
    // This is an example of how a future is executed and fulfilled.
    DispatchQueue.global(qos: .background).async {
        // lets make an expensive operation on a background thread.
        // The success and progress and error callbacks will be synced on the main thread
        // So no need to sync back to the main thread.

        do {
            // Do an expensive operation here ....
            let resizedImage = try image.resize(ratio: 16/9)

            // If possible, we can send smaller progress updates
            // Otherwise it's a good idea to send 1 to indicate this task is all finished.
            // Not sending this won't cause any harm but your progress callback will not be triggered as a result of this future.
            future.update(progress: 1)
            future.succeed(with: resizedImage)
        } catch {
            future.fail(with: error)
        }
    }
})

注意:您也可以使用现有 future 的 then 回调,该回调在后台线程上执行。

编码

PiuPiu 为您提供了一些便利方法,可以将对象编码成 JSON 并添加到 BasicRequest 对象中。

编码 JSON 字符串

request.setJSONBody(string: jsonString, encoding: .utf8)

编码 JSON 对象

let jsonObject: [String: Any?] = [
    "id": "123",
    "name": "Kevin Malone"
]

try request.setJSONBody(jsonObject: jsonObject)

编码 Encodable

try request.setJSONBody(encodable: myCodable)

将编码包装在 ResponseFuture 中

将请求创建包装在 ResponseFuture 中可能有益。这将允许您

  1. 在提交请求时延迟请求创建到稍后的时间。
  2. 将创建请求时抛出的任何错误组合到错误回调中。
dispatcher.dataFuture(from: {
    let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
    var request = URLRequest(url: url, method: .post)
    try request.setJSONBody(post)
    return request
}).error({ error in
    // Any error thrown while creating the request will trigger this callback.
}).send()

解码

解包 Data

这将为您解包数据对象或抛出 ResponseError 如果它不存在。这很方便,这样您就不必处理那些讨厌的可选值。

dispatcher.dataFuture(from: request).response({ response in
    let data = try response.unwrapData()

    // do something with data.
    print(data)
}).error({ error in 
    // Triggered when the data object is not there.
}).send()

解码 String

dispatcher.dataFuture(from: request).response({ response in
    let string = try response.decodeString(encoding: .utf8)

    // do something with string.
    print(string)
}).error({ error in
    // Triggered when decoding fails.
}).send()

解码 Decodable

dispatcher.dataFuture(from: request).response({ response in
    let posts = try response.decode([Post].self)

    // do something with the decodable object.
    print(posts)
}).error({ error in
    // Triggered when decoding fails.
}).send()

内存管理

ResponseFuture 可能存在三种强引用类型。

  1. 在调用 send() 之后,系统可能保留对 ResponseFuture 的强引用。这种引用是临时的,一旦系统返回响应,就会解除分配。这永远不会创建循环引用,但由于未来被系统保留,它将不会在收到响应或触发错误之前释放。
  2. 任何引用 self 的回调都有一个对 self 的强引用,除非显式指定 [weak self]
  3. 开发人员对 ResponseFuture 的强引用。

强回调

当只有 12 适用于您的情况时,将创建一个临时的循环引用,直到未来得到解决。在这种情况下,您可能希望使用 [weak self],但不强制要求。

dispatcher.dataFuture(from: request).then({ response -> [Post] in
    // [weak self] not needed as `self` is not called
    return try response.decode([Post].self)
}).response({ posts in
    // [weak self] not needed but may be added. There is a temporary reference which will hold on to self while the request is being made.
    self.show(posts)
}).send()

警告 使用 [weak self] 时,不要强制解包 self,也永远不要强制解包 self 上的任何内容。这样只会引发崩溃。

!! 不要这样做。 !! 永远不要这样做。即使你是编程天才也不行。这只会惹麻烦。

dispatcher.dataFuture(from: request).success({ response in
    // We are foce unwrapping a text field! DO NOT DO THIS!
    let textField = self.textField!

    // If we dealocated textField by the time the 
    // response comes back, a crash will occur
    textField.text = "Success"
}).send()

如果在回调中强制解包任何内容,您将遇到崩溃(即使用 !)。我们建议您始终避免在回调中强制解包任何内容。

在开始使用前,始终先解包您的对象。这包括系统生成的任何 IBOutlet。使用 guard、使用 assert。不要使用 !

模拟调度器

测试网络调用一直都很痛苦。这就是为什么我们包括了 MockURLRequestDispatcher。它允许您在不实际进行网络调用的同时模拟网络响应。

以下是如何使用它的一个示例

private let dispatcher = MockURLRequestDispatcher(delay: 0.5, callback: { request in
    if let id = request.integerValue(atIndex: 1, matching: [.constant("posts"), .wildcard(type: .integer)]) {
        let post = Post(id: id, userId: 123, title: "Some post", body: "Lorem ipsum ...")
        return try Response.makeMockJSONResponse(with: request, encodable: post, statusCode: .ok)
    } else if request.pathMatches(pattern: [.constant("posts")]) {
        let post = Post(id: 123, userId: 123, title: "Some post", body: "Lorem ipsum ...")
        return try Response.makeMockJSONResponse(with: request, encodable: [post], statusCode: .ok)
    } else {
        throw ResponseError.notFound
    }
})

注意: 您应该对调度器有一个强引用,并在回调中对 self 有一个弱引用。

未来功能

  • 并行调用
  • 顺序调用
  • 更通用的调度器。响应对象过于特定
  • 更好的多线程支持
  • 请求取消

依赖项

PiuPiu 包括...没有东西。这是一个轻量级的库。

致谢

PiuPiu 由 Jacob Sikorski 所拥有和维护。

许可证

PiuPiu 采用 MIT 许可证发布。详情请查看 LICENSE