PewPew
以前称为 NetworkKit,该项目被重命名为支持 CocoaPods。PewPew 为 iOS 添加了 Futures
(即:Promises
)的概念。它的目的是使网络调用更简单、更干净,并为开发者提供比其他任何网络框架都多的可定制性。
问(Q):我应该使用这个框架吗?答(A):因为,您喜欢干净代码。
问(Q):为什么名字这么愚蠢?答(A):因为“哔哔”是激光的声音。激光来自未来。
问(Q):哪种熊最好?答(A):错误!黑熊!
更新
1.1.0
移除了默认翻译。
要迁移到这个,您 应该 通过扩展 ResponseError
、RequestError
和 SerializationError
来包含您自己的翻译,这些扩展符合 LocalizedError
和(可选)CustomNSError
要重新引入以前的行为,您应该包括这里找到的文件和本地化 这里 和本地化 这里
1.0.1
修复了翻译时的崩溃问题
特性
- 网络请求的包装器
- 使用
Futures
(即Promises
)以实现可扩展性和简洁性 - 提供用于反序列化 Decodable 和 JSON 的便捷方法
- 易于集成
- 处理常见的 http 错误
- 返回生产环境安全错误信息
- 强类型和安全解包响应
- 易于扩展。可以轻松与
Alamofire
、ObjectMapper
和MapCodableKit
等框架协同工作 - 简洁!
安装
Carthage
Carthage 是一个去中心化的依赖管理器,它构建您的依赖关系并提供二进制框架。
您可以使用以下命令通过 Homebrew 安装 Carthage:
$ brew update
$ brew install carthage
要使用 Carthage 将 PewPew 集成到您的 Xcode 项目中,请在您的 Cartfile
中指定它。
github "cuba/PewPew" ~> 1.0
运行 carthage update
以构建框架,并将构建的 PewPew.framework
拖动到您的 Xcode 项目中。
使用方法
PewPew
1. 在您的文件中导入 import PewPew
ServerProvider
2. 实现 服务器提供者提供服务器 URL。不使用简单 URL 的原因是为了让您可以动态更改 URL。例如,如果您有一个环境选择器,每次您更改环境时,您都必须重新创建调度器。最简单创建服务器提供者方法是仅在实际的 ViewController 上实现该协议。
extension ViewController: ServerProvider {
var baseURL: URL {
return URL(string: "https://example.com")!
}
}
但是您可以选择使用单独的对象来实现服务提供者,或者创建一个单例对象以便在整个应用程序中共享。因为 NetworkDispatcher
上服务提供者的引用是弱引用,所以您无需担心任何循环引用。
3. 发起请求。
现在我们已经设置了 ServerProvider
,我们可以开始进行 API 调用了。
let dispatcher = NetworkDispatcher(serverProvider: self)
let request = BasicRequest(method: .get, path: "/posts")
dispatcher.future(from: request).response({ response in
// Handles all 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 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
// ...
}).error({ error in
// Handles any errors during the request process,
// including anything thrown in any of the callback (except this one).
}).completion({
// The completion callback is guaranteed to be called once
// for every time the `start()` or `send()` method is triggered on the future.
//
}).start()
注意:如果您没有调用 start()
,则不会发生任何事。注意:如果您多次调用 start()
,可能会发生奇怪的事情。请勿这样做。
4. 分隔关注点与转换未来
这个双关语并非有意为之(诚实地说)
现在让我们把将我们对象进行解码的部分移动到另一个方法中。这样,我们的业务逻辑就不会与我们的序列化逻辑混淆。使用未来的一个伟大的之处在于我们可以返回它们!
让我们创建一个类似下面的方法
private func getPosts() -> ResponseFuture<[Post]> {
let dispatcher = NetworkDispatcher(serverProvider: self)
let request = BasicRequest(method: .get, path: "/posts")
// We create a future and tell it to transform the response using the
// `then` callback.
return dispatcher.future(from: request).then({ response -> [Post] in
// This callback transforms our response to another type
// We can still handle errors the same way as we did before.
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
}
// 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()
。
然后我们可以这样简单地调用它
getPosts().response({ posts 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()
Future
您已经看到,ResponseFuture
允许您链式调用回调,转换响应对象并将其传递。但是,除了上述简单示例之外,还有更多的事情可以帮助您编写出更干净的代码!
dispatcher.future(from: request).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
回调
response
回调函数在收到请求且任何链式回调(如 then
或 join
)未抛出错误时被触发。在回调序列的末尾,这将为你提供你的转换“承诺”返回的确切内容。
dispatcher.future(from: request).response({ response in
// Triggered when a response is recieved and all callbacks succeed.
})
注意:此方法应仅调用一次。
error
回调
这可以被认为是一个 do
块中的 catch
。从你触发 send()
的那一刻起,每当回调序列中抛出错误时,错误回调就会被触发。这包括在任何其他回调中抛出的错误。
dispatcher.future(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.future(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
类型的值转换为另一个类型。
dispatcher.future(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.future(from: request).then({ response -> Post in
return try response.decode(Post.self)
}).replace({ 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.
})
join
回调
此回调将未来转换成另一个类型,包含其原始结果以及返回回调的结果。这使我们能够在系列中执行异步调用。
dispatcher.future(from: request).then({ response -> Post in
return try response.decode(Post.self)
}).join({ post -> ResponseFuture<User> in
// Joins a future with another one returning both results
return self.fetchUser(forId: post.userId)
}).response({ post, user in
// The final response callback includes both results.
})
send
或 start
这将启动 ResponseFuture
。换句话说,会触发 action
回调并将请求发送到服务器。
注意:如果在调用此方法之前未调用,将不会发生任何操作(不会发起请求)。注意:此方法应仅在声明所有回调(成功
,失败
,错误
,then
等)之后调用。注意:这种方法应仅调用一次。
创建自定义 ResponseFuture
你可以出于各种原因创建自己的 ResponseFuture。如果你这样做,你将获得迄今为止看到的所有好处。
以下是一个示例响应未来,它在另一个线程中执行解码。
return ResponseFuture<[Post]>(action: { future in
// This is an example of how a future is executed and
// fulfilled.
DispatchQueue.global(qos: .userInitiated).async {
// lets make an expensive operation on a background thread.
// The below is just an example of how you can parse on a seperate thread.
do {
// Do an expensive operation here ....
let posts = try response.decode([Post].self)
DispatchQueue.main.async {
// We should syncronyze the result back to the main thread.
future.succeed(with: posts)
}
} catch {
// We can handle any errors as well.
DispatchQueue.main.async {
// We should syncronize the error to the main thread.
future.fail(with: error)
}
}
}
})
注意:在成功或失败未来之前,你应该始终在主线程上同步结果。
编码
PewPew 为你了提供了一些方便的方法,您可以将对象编码为 JSON 并将其添加到 BasicRequest
对象中。
String
编码 JSON var request = BasicRequest(method: .post, path: "/users")
request.setJSONBody(string: jsonString, encoding: .utf8)
编码 JSON 对象
let jsonObject: [String: Any?] = [
"id": "123",
"name": "Kevin Malone"
]
var request = BasicRequest(method: .post, path: "/users")
try request.setJSONBody(jsonObject: jsonObject)
Encodable
编码 var request = BasicRequest(method: .post, path: "/posts")
try request.setJSONBody(encodable: myCodable)
Data
对象)
自定义编码(通过设置 var request = BasicRequest(method: .post, path: "/users")
request.httpBody = myData
将编码包装在 ResponseFuture 中
将请求创建包装在 ResponseFuture 中可能会有好处。这样你可以
- 在稍后的时间提交请求时延迟请求创建。
- 在错误回调中合并创建请求时抛出的任何错误。
dispatcher.future(from: {
var request = BasicRequest(method: .post, path: "/posts")
try request.setJSONBody(myCodable)
return request
}).error({ error in
// Any error thrown while creating the request will trigger this callback.
}).send()
解码
Data
解包 这将为您解包数据对象或在没有找到时抛出 ResponseError。这样可以方便地处理那些讨厌的可选值。
dispatcher.future(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.future(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()
可解码
解码 dispatcher.future(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
可能存在 3 种强引用类型。
- 在调用
send()
之后,系统可能对ResponseFuture
有一个强引用。这个引用是临时的,一旦系统返回响应,就会释放。这绝对不会创建循环引用,但是由于承诺被系统持有,它不会在收到响应或触发错误之前释放。 - 任何引用
self
的回调都有一个对self
的强引用,除非显式指定[weak self]
。 - 开发者为
ResponseFuture
创建自己的强引用。
强回调
当且仅当 1 和 2 适用于您的情况时,不会创建循环引用。然而,作为 self
的对象引用会保持强有力的(暂时性的)直到请求返回或者抛出错误。在这种情况下,您可能希望使用 [weak self]
,但它不是必须的。
dispatcher.future(from: request).then({ response -> [Post] in
// [weak self] not needed as `self` is not called but it doesn't hurt
return try response.decode([Post].self)
}).response({ posts in
self.show(posts)
}).send()
警告 如果您使用 [weak self]
,不要强制解包 self
,也永远不要强制解包 self
上的任何内容。那只会导致崩溃。
!! 不要这样做!! 永远不要这样做。即使你是编程天才也请不要这样做。这只会带来问题。
dispatcher.future(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。使用任何不是 !
的东西。
ResponseFuture
的强引用
对 您可能正在引用您的ResponseFuture
。只要您确保回调是弱引用,以避免循环引用,这没问题。
self.postResponseFuture = dispatcher.future(from: request).then({ response in
// [weak self] not needed as `self` is not called
let posts = try response.decode([Post].self)
return SuccessResponse<[Post]>(data: posts, response: response)
}).response({ [weak self] response in
// [weak self] needed as `self` is called
self?.show(response.data)
}).completion({ [weak self] in
// [weak self] needed as `self` is called
self?.postResponseFuture = nil
})
// Perform other logic, add delay, do whatever you would do that forced you
// to store a reference to this ResponseFuture in the first place
self.postResponseFuture?.send()
警告:如果您强烈持有未来对象,但未使用[weak self]
将 self 设置为弱引用,您肯定会遇到循环引用。以下是一个不应遵循的示例:
!!千万不要这样做!!
self.strongResponseFuture = dispatcher.future(from: request).response({ response in
// Both the `ResponseFuture` and `self` are held on by each other.
// `self` will never be dealocated and neither will the future!
self.show(response.data)
}).send()
如果self
对您的ResponseFuture
有强引用,并且ResponseFuture
通过任何回调对self
有强引用,您已经创建了一个循环引用。都不会被释放。
ResponseFuture
的弱引用
对您可以选择对ResponseFuture有一个弱引用。只要您在调用send()
之后这样做就可以,因为您的对象在您有机会这样做之前就会被释放。
self.weakResponseFuture = dispatcher.future(from: request).completion({
// Always triggered
}).send()
// This ResponseFuture may or may not be nil at this point.
// This depends on if the system is holding on to the
// ResponseFuture as it is awaiting a response.
// but the callbacks will always be triggered.
注意以下是一个永远不会发生请求的示例,因为在触发send()
之前我们失去了对ResponseFuture的引用
千万不要这样做:
self.weakResponseFuture = dispatcher.future(from: request).completion({
// [weak self]
expectation.fulfill()
})
// WHOOPS!!!!
// Our object is already nil because we have not established a strong reference to it.
// The `send()` method will do nothing and no callback will be triggered.
self.weakResponseFuture?.send()
自定义编码
您可以将BasicRequest扩展到为任何类型对象添加编码。
ObjectMapper
ObjectMapper
不包含在框架中。这是为了让那些不想使用它的框架变得更轻量。但如果有需要,您可以轻松添加对ObjectMapper
的编码支持。以下是一个示例,说明您可以如何添加对对象和数组的BaseMappable
(Mappable
和ImmutableMappable
)编码支持。
extension BasicRequest {
/// Add JSON body to the request from a `BaseMappable` object.
///
/// - Parameters:
/// - mappable: The `BaseMappable` object to serialize into JSON.
/// - context: The context of the mapping object
/// - shouldIncludeNilValues: Wether or not we should serialize nil values into the json object
mutating func setJSONBody<T: BaseMappable>(mappable: T, context: MapContext? = nil, shouldIncludeNilValues: Bool = false) {
let mapper = Mapper<T>(context: context, shouldIncludeNilValues: shouldIncludeNilValues)
guard let jsonString = mapper.toJSONString(mappable) else {
return
}
self.setJSONBody(string: jsonString)
}
/// Add JSON body to the request from a `BaseMappable` array.
///
/// - Parameters:
/// - mappable: The `BaseMappable` array to serialize into JSON.
/// - context: The context of the mapping object
/// - shouldIncludeNilValues: Wether or not we should serialize nil values into the json object
mutating func setJSONBody<T: BaseMappable>(mappable: [T], context: MapContext? = nil, shouldIncludeNilValues: Bool = false) {
let mapper = Mapper<T>(context: context, shouldIncludeNilValues: shouldIncludeNilValues)
guard let jsonString = mapper.toJSONString(mappable) else {
return
}
self.setJSONBody(string: jsonString)
}
}
MapCodableKit
MapCodableKit是一个轻量级的JSON解析框架。
类似于MapCodableKit
,此框架上的支持也已不再可用。但与ObjectMapper
一样,您可以轻松地重新添加对MapEncodable
的支持。
extension BasicRequest {
/// Add body to the request from a `MapEncodable` object.
///
/// - Parameters:
/// - mapEncodable: The `MapEncodable` object to serialize into JSON.
/// - options: Writing options for serializing the `MapEncodable` object.
/// - Throws: Any serialization errors thrown by `MapCodableKit`.
mutating public func setJSONBody<T: MapEncodable>(mapEncodable: T, options: JSONSerialization.WritingOptions = []) throws {
ensureJSONContentType()
self.httpBody = try mapEncodable.jsonData(options: options)
}
}
自定义解码
类似于编码,你也可以为所使用的任何解码器添加解码支持,包括通过扩展 ResponseInterface
来添加对 ObjectMapper
的支持。
ObjectMapper
extension ResponseInterface where T == Data? {
/// Attempt to Decode the response data into an BaseMappable object.
///
/// - Returns: The decoded object
func decodeMappable<D: BaseMappable>(_ type: D.Type, context: MapContext? = nil) throws -> D {
let jsonString = try self.decodeString()
let mapper = Mapper<D>(context: context)
guard let result = mapper.map(JSONString: jsonString) else {
throw SerializationError.failedToDecodeResponseData(cause: nil)
}
return result
}
/// Attempt to decode the response data into a BaseMappable array.
///
/// - Returns: The decoded array
func decodeMappable<D: BaseMappable>(_ type: [D].Type, context: MapContext? = nil) throws -> [D] {
let jsonString = try self.decodeString()
let mapper = Mapper<D>(context: context)
guard let result = mapper.mapArray(JSONString: jsonString) else {
throw SerializationError.failedToDecodeResponseData(cause: nil)
}
return result
}
}
MapCodableKit
MapCodableKit是一个轻量级的JSON解析框架。
extension ResponseInterface where T == Data? {
/// Attempt to deserialize the response data into a MapDecodable object.
///
/// - Returns: The decoded object
func decodeMapDecodable<D: MapDecodable>(_ type: D.Type) throws -> D {
let data = try self.unwrapData()
do {
// Attempt to deserialize the object.
return try D(jsonData: data)
} catch {
// Wrap this error so that we're controlling the error type and return a safe message to the user.
throw SerializationError.failedToDecodeResponseData(cause: error)
}
}
/// Attempt to decode the response data into a MapDecodable array.
///
/// - Returns: The decoded array
func decodeMapDecodable<D: MapDecodable>(_ type: [D].Type) throws -> [D] {
let data = try self.unwrapData()
do {
// Attempt to deserialize the object.
return try D.parseArray(jsonData: data)
} catch {
// Wrap this error so that we're controlling the error type and return a safe message to the user.
throw SerializationError.failedToDecodeResponseData(cause: error)
}
}
}
Mock Dispatcher
测试网络请求总是一件痛苦的事情。这就是为什么我们包含了 MockDispatcher
。它允许你在不实际发出网络请求的情况下模拟网络响应。
let url = URL(string: "https://jsonplaceholder.typicode.com")!
let dispatcher = MockDispatcher(baseUrl: url, mockStatusCode: .ok)
let request = BasicRequest(method: .get, path: "/posts")
try dispatcher.setMockData(codable)
/// The url specified is not actually called.
dispatcher.future(from: request).send()
今后功能
- 并行调用
- 串行调用
- 自定义翻译
- 更多未来式的请求创建
- 更通用的派送器。响应对象过于具体。
- 更好的多线程支持
依赖
PewPew 包含...没有东西。这是一个轻量级库。
致谢
PewPew 由 Jacob Sikorski 拥有和维护。
许可证
PewPew采用MIT许可证发布。请参阅LICENSE以了解详细信息