PewPew 1.1.0

PewPew 1.1.0

Jacob Sikorski 维护。



PewPew 1.1.0

  • Jacob Sikorski

Swift 5 iOS 8+ Carthage CocoaPods GitHub Build

PewPew

以前称为 NetworkKit,该项目被重命名为支持 CocoaPods。PewPew 为 iOS 添加了 Futures(即:Promises)的概念。它的目的是使网络调用更简单、更干净,并为开发者提供比其他任何网络框架都多的可定制性。

问(Q):我应该使用这个框架吗?答(A):因为,您喜欢干净代码。

问(Q):为什么名字这么愚蠢?答(A):因为“哔哔”是激光的声音。激光来自未来。

问(Q):哪种熊最好?答(A):错误!黑熊!

更新

1.1.0

移除了默认翻译。

要迁移到这个,您 应该 通过扩展 ResponseErrorRequestErrorSerializationError 来包含您自己的翻译,这些扩展符合 LocalizedError 和(可选)CustomNSError

要重新引入以前的行为,您应该包括这里找到的文件和本地化 这里 和本地化 这里

1.0.1

修复了翻译时的崩溃问题

特性

  • 网络请求的包装器
  • 使用 Futures(即 Promises)以实现可扩展性和简洁性
  • 提供用于反序列化 Decodable 和 JSON 的便捷方法
  • 易于集成
  • 处理常见的 http 错误
  • 返回生产环境安全错误信息
  • 强类型和安全解包响应
  • 易于扩展。可以轻松与 AlamofireObjectMapperMapCodableKit 等框架协同工作
  • 简洁!

安装

Carthage

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

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

$ brew update
$ brew install carthage

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

github "cuba/PewPew" ~> 1.0

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

使用方法

1. 在您的文件中导入 PewPew

import PewPew

2. 实现 ServerProvider

服务器提供者提供服务器 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 回调函数在收到请求且任何链式回调(如 thenjoin)未抛出错误时被触发。在回调序列的末尾,这将为你提供你的转换“承诺”返回的确切内容。

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

sendstart

这将启动 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 对象中。

编码 JSON String

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 中可能会有好处。这样你可以

  1. 在稍后的时间提交请求时延迟请求创建。
  2. 在错误回调中合并创建请求时抛出的任何错误。
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 种强引用类型。

  1. 在调用 send() 之后,系统可能对 ResponseFuture 有一个强引用。这个引用是临时的,一旦系统返回响应,就会释放。这绝对不会创建循环引用,但是由于承诺被系统持有,它不会在收到响应或触发错误之前释放。
  2. 任何引用 self 的回调都有一个对 self 的强引用,除非显式指定 [weak self]
  3. 开发者为 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的编码支持。以下是一个示例,说明您可以如何添加对对象和数组的BaseMappableMappableImmutableMappable)编码支持。

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以了解详细信息