PiuPiu
之前被称为 NetworkKit,该项目的名称已被更改为支持 CocoaPods。PiuPiu 为 iOS 增加了 Future
(也称为:Promise
)的概念。其目的是使网络调用更加简洁和简单,并为开发者提供比其他任何网络框架更多的可定制性。
问题:为什么我应该使用这个框架?答案:因为,你喜欢干净代码。
问题:为什么这个名字这么傻?答案:因为“piu piu”是激光的声音。而激光来自未来。
问题:哪种熊最好?答案:错误!黑熊!
更新
1.4.0
- 将
Request
协议更改为返回URLRequest
- 用
RequestSerializer
替换Dispatcher
和NetworkDispatcher
。 - 回调只会触发一次。一旦触发回调,其引用就会被释放(置为 null)。
- 这是为了防止内存泄漏。
- 添加了使用基本
URLRequest
的DataDispatcher
、UploadDispatcher
和DownloadDispatcher
协议。- 添加了实现所有 3 个协议的
URLRequestDispatcher
类。 - 包括
MockURLRequestDispatcher
在内的分配器上的回调都是弱引用。现在您必须拥有对分配器的引用。 - 在分配器被释放时取消请求。
- 添加了实现所有 3 个协议的
- 向
ResponseFuture
添加了cancellation
回调。- 这可以通过使用
cancel
或在任何join
(序列仅)、then
、replace
或action
(初始化)回调中返回 nil 来手动触发。 - 此回调不会取消实际请求,只是停止在最后的
cancellation
和completion
回调之后继续执行ResponseFuture
的任何进一步执行。
- 这可以通过使用
- 添加了不传递响应对象的并行
join
回调。此回调是非逃逸的。 - 稍微更好的多线程支持。
- 默认情况下,
then
在后台线程上触发。 success
、response
、error
、completion
和cancellation
回调函数始终在主线程上同步。
- 默认情况下,
- 通过
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项目中。
用法
PiuPiu
导入您的文件
1. 将 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()
回调
response
或 success
回调
response
回调在请求接收且链式回调(如 then
或 join
)中没有抛出错误时触发。在回调序列的末尾,这给您提供了您希望 "承诺" 返回的精确内容。
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]
。
send
或 start
这将为 ResponseFuture
启动。换句话说,将触发 action
回调并将请求发送到服务器。
注意:如果未调用此方法,则不会发生任何事情(不会进行请求)。注意:应在声明所有回调(success
、failure
、error
、then
等等)之后调用此方法。
ResponseFuture
创建自己的 出于各种原因,您可以创建自己的 ResponseFuture。这可以在另一个 future 的 join
或 replace
回调中使用,以便进行一些链式操作。
以下是一个示例,展示了一个在另一个线程中进行开销较大的操作的响应 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 中可能有益。这将允许您
- 在提交请求时延迟请求创建到稍后的时间。
- 将创建请求时抛出的任何错误组合到错误回调中。
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
可能存在三种强引用类型。
- 在调用
send()
之后,系统可能保留对ResponseFuture
的强引用。这种引用是临时的,一旦系统返回响应,就会解除分配。这永远不会创建循环引用,但由于未来被系统保留,它将不会在收到响应或触发错误之前释放。 - 任何引用
self
的回调都有一个对self
的强引用,除非显式指定[weak self]
。 - 开发人员对
ResponseFuture
的强引用。
强回调
当只有 1
和 2
适用于您的情况时,将创建一个临时的循环引用,直到未来得到解决。在这种情况下,您可能希望使用 [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