Remotli 1.0.0

Remotli 1.0.0

Seth Arnott 维护。



Remotli 1.0.0

  • 作者
  • Seth Arnott

Remotli 框架

Remotely 是一个 Swift 框架,旨在简化从基于 Web 的 API 加载远程 JSON 资源并将它们转换为 iOS 应用程序中的模型对象的过程。

功能

  • 使用 Swift 4.0 编写
  • 易于使用
  • 高度可配置

安装

  1. 添加主 spec 库源 URL
  2. 在 Podfile 中指定目标(s)的正确版本。
    • pod 'Remotli', '~> 1.0'
  3. 运行 pod update(可能需要先运行 pod install)
  4. 打开工作区,清理,并构建

示例 Podfile


platform :ios, '10.0'
use_frameworks!

source 'https://github.com/CocoaPods/Specs.git'

# Example dependencies
target 'MyAppTargetName' do
    
    pod 'Remotli', '~> 1.0.0'

end

用法

您可以使用几种方法来使用 Remotly。它旨在无需设置即可直接使用,但在需要时也提供高度配置的灵活性。

最简单用例

使用 Remotely 的最简单方式涉及 2 个步骤。这假设您已经在自己的应用程序中有一个数据模型,该模型对应于您从远程 API 预期收到的 json 响应。您的模型对象必须符合 Codable 协议,才能在使用 Remotely 时进行解析。

  • 使用您的主机名声明一个 Remotli 实例。
    • 它只应该是主机名,不包含方案前缀。
    • 使用 Remotli(host:) 初始化器,方案将自动设置为 https://。
  • 在您的 Remotli 实例上调用 'send' 并传入任何必要的参数。
    • 完成处理程序将使用命名参数,第一个是您的模型对象类型,第二个是错误类型。
    • 这两个参数都是可选类型。
/// Defines a sample user model for parsing with JSON.
/// Note the conformance to the Codable protocol.
public class User: Codable {
    public var identifier: String
    public var firstName: String
    public var lastName: String
    public var email: String
    public var phone: String?
    public var birthday: String?
    public var isPendingVerification: Bool = false
}

extension User {
    // If your remote api uses different json keys for some values than you plan to use
    // for your local model object, you will need to define coding keys that map from the
    // local model to the remote api model. The case name should match your local model
    // var names, and the string key should match the remote api key for this value.
    // If your local var name matches the remote api key do not assign a string value.
    enum CodingKeys: String, CodingKey {
        case identifier
        case firstName = "first_name"
        case lastName = "last_name"
        case email
        case phone
        case birthday
        case isPendingVerification = "pending_verification"
    }
}

/// Create an instance of Remotli using your host name. 
/// This convenience init defaults the scheme to https.
internal var myHost = Remotli(host: "my-remote-api.com")

/// Call 'send' on your remote host, passing in any required parameters.
/// Note the use of the custom 'User' model type as the type for the first
/// completion parameter.
myHost.send(
    "/user/login", 
    port: nil, 
    method: HTTPMethod.get, 
    mimeType: MimeType.applicationJson, 
    headers: nil, 
    parameters: ["username": username, "password": password], 
    usingAuth: false, 
    completion: { (user: User?, error: Error?) in

        // validate the response, check for errors
        guard let user = user, error == nil else {
            // do something with the error here
            return
        }

        // do something with user
    })

如果您需要自定义行为或实现更灵活,请继续阅读。

概述

本节提供了关于此框架工作原理的概述,以及可供更高级配置和定制的选项,以适应您的需求。

路由

Remotli 是围绕可路由的,即表示远程 API上资源的符合 Routable 协议的对象设计的。Remotli 包含一个名为 Route 的可路由的默认实现。Route 对象封装了生成远程服务 URL 请求所需的所有信息。

Routable 协议和 Route 对象看起来像这样

public protocol Routable {
    var uri: String { get }
    var port: Int? { get }
    var method: HTTPMethod { get }
    var mimeType: MimeType { get }
    var headers: [String: String]? { get }
    var parameters: [String: Any]? { get }
    var requiresAuth: Bool { get }

    var timeout: TimeInterval { get }
    var cachePolicy: URLRequest.CachePolicy { get }
}

extension Routable {
    public var timeout: TimeInterval {
        return 60
    }

    public var cachePolicy: URLRequest.CachePolicy {
        return .useProtocolCachePolicy
    }

    public func route() -> Route {
        return Route(uri: uri, port: port, method: method, mimeType: mimeType, headers: headers, parameters: parameters, requiresAuth: requiresAuth)
    }
}

open class Route: Routable {
    public var uri: String
    public var port: Int?
    public var method: HTTPMethod
    public var mimeType: MimeType
    public var headers: [String : String]?
    public var parameters: [String : Any]?
    public var requiresAuth: Bool

    public init(uri: String, port: Int? = nil, method: HTTPMethod = .post, mimeType: MimeType = .applicationJson, headers: [String: String]? = nil, parameters: [String: Any]? = nil, requiresAuth: Bool = false) {
        self.uri = uri
        self.port = port
        self.method = method
        self.mimeType = mimeType
        self.headers = headers
        self.parameters = parameters
        self.requiresAuth = requiresAuth
    }
}

Remotely

处理路由和发送请求的主要类是 Remotli 类。此类为 Remotely 协议提供了默认实现,该协议定义了处理路由所需的操作。

Remotely 协议指定了处理 Routables 的接口,并提供两个将请求发送到已配置远程主机的函数。

// Option 1
// takes parameters that are used to create a Route object behind the scenes, then passes the route to the 'send' function
// specified in option 2.
myHost.send(_ path:port:method:mimeType:headers:parameters:usingAuth:completion:)

// Option 2
// takes a route object directly
myHost.send(requestFor:completion:)

第一个函数提供了 Remotli 的最简单用法,因为您根本不需要直接接触 Routables 或 Route 对象。您只需指定相对路径,提供远程主机所需的所有值。

myHost.send(
    "/user/login", 
    port: nil, 
    method: HTTPMethod.get, 
    mimeType: MimeType.applicationJson, 
    headers: nil, 
    parameters: ["username": username, "password": password], 
    usingAuth: false, 
    completion: { (user: User?, error: Error?) in
    })

第二个函数通过接受默认的Route对象或你自己定义和定制的其他Routable对象,提供了更高的灵活性。以下4个示例展示了根据你的需求生成路由的可能方法。

自定义Routables

本例展示了如何使用Remotli提供的默认Route类来创建自定义路由。

// note that parameters where the default values are sufficient are omitted
let route = Route(
    uri: "/api/mas/0.2/app_config/fetch",
    mimeType: .applicationFormUrlEncoded,
    parameters: defaultParameters()
    )
    
// call 'send(requestFor:completion:)' passing in the route
myHost.send(requestFor: route, completion: completion)

如果你需要封装你的路由或者在路由生成中使用自定义逻辑,你可以定义自己的类以符合Routable协议。

// This demonstrates a custom class conforming to the ` Routable ` protocol to create a custom route.
class ConfigurationRoute: Routable {

    var method: HTTPMethod = .post
    var uri: String =  "/api/mas/0.2/app_config/fetch"
    var mimeType: MimeType = .applicationFormUrlEncoded
    var port: Int?
    var headers: [String: String]?
    var parameters: [String: Any]?
    var requiresAuth: Bool = false

    init(parameters: [String: Any]?) {
        self.parameters = parameters
    }
}

// send the request using the custom route
send(requestFor: 
        ConfigurationRoute(parameters: ["app_id": "MY_APP_ID", "token": ""]), 
        completion: completion
    )

如果你有一个端点能够根据不同的传入参数提供不同响应的远程服务,并且变量(由Routable定义)在任何时刻都不需要改变或更新(只读),你可以使用符合Routable协议的枚举来定义你的路由。

// This is a sample route that makes use of an enum to generate differing parameter
// options sent to the same token endpoint depending on the type of token desired.
enum TokenRoute: Routable {

    // each case in this route represents a different token type that can
    // be requested from this api route or endpoint. All cases share the same
    // method, uri, mimeType and headers but send different parameters to the
    // remote host.
    case anonymous
    case refresh(token: String)
    case credential
    case social

    var method: HTTPMethod {
        return .post
    }

    var uri: String {
        return "/v1/tokens"
    }

    var mimeType: MimeType {
        return .applicationJson
    }

    var port: Int? {
        return nil
    }

    var headers: [String: String]? {
        return [       
            "Accept-Encoding": "gzip",
            "Accept-Language": "en-us",
            "Accept": "application/json"
        ]
    }

    var parameters: [String: Any]? {
        switch self {
        case .anonymous: return ["type": "ANONYMOUS"]
        case .refresh(let token): return ["type": "REFRESH", "credential": token]
        case .credential: return ["type": "CREDENTIAL"]
        case .social: return ["type": "SOCIAL"]
        }
    }

    var requiresAuth: Bool {
        return false
    }
}

// a sample function that might make use of the custom TokenRoute might look like this

// Fetches the token from the remote host, making use of a route initializer 
// and specifying that an anonymous token is being requested.
func getToken(completion: @escaping (Token?, Error?) -> Void) {
    send(requestFor: TokenRoute.anonymous, completion: completion)
}

你也可以定义一个Route类的子类来为路由提供你自己的自定义行为。

以下示例展示了使用UserRoute类,该类使用静态变量来指定默认路由信息,并使用静态函数来创建和返回UserRoute的实例,以便于用户登录。请注意,静态初始化函数生成请求的自定义参数,而登录函数只需调用静态初始化函数并提供设置参数的值。

// Defines a class based route that uses static functions as custom initializers.
// This is an alternate method to using an enum.
class UserRoute: Route {
    
    // static vars define default parameters for static route initializers
    static let uri: String = "/v1/tokens"
    static let method: HTTPMethod = .post
    static let mimetype: MimeType = .applicationJson
    static var defaultHeaders: [String: String] = [
        "Accept-Encoding": "gzip",
        "Accept-Language": "en-us",
        "Accept": "application/json"
        ]
}

extension UserRoute {

    // custom Route initializer that sets up a login route with custom parameters
    static func login(username: String, password: String) -> UserRoute {
        return UserRoute(
            uri: uri,
            port: nil,
            method: method,
            mimeType: mimetype,
            headers: defaultHeaders,
            parameters: [
                "name": username,
                "credentialType": "CREDENTIAL",
                "credential": password
            ],
            requiresAuth: false
        )
    }
}

// a sample function that might make use of the custom UserRoute might look like this

// Logs in to this remote host and returns a User model object if successful.
func login(username: String, password: String, completion: @escaping (User?, Error?) -> Void) {
    let route = UserRoute.login(username: username, password: password)
    send(requestFor: route, completion: completion)
}
自定义Remotli实现

可能会有这种情况,当你使用默认的Remotli类时没有提供一些额外的自定义行为。

你也可能希望封装对特定远程Web服务的所有调用,以拥有自己对远程API的内部表示。有几个方法可以实现这一目标。

你可以通过继承Remotli类实现自己的封装服务API。例如,如果你想要访问Flickr API,你可以创建一个特定的 subclasses专门用于发送Flickr请求,并在该子类内编写所有的访问函数。

如果你想要更精细的控制或自定义行为,你可以创建自己的类,使其符合Remotely协议,并实现你需要的行为。

以下示例展示了一个子类Remotli的自定义远程Web服务,因为它需要额外的配置。该子类封装了远程服务的所有调用。额外的配置是通过覆盖'setup()'函数来处理的。该函数在Remotli中实现为子类的挂钩,用于自定义类,并在初始化后立即调用。

class MySiteAPI: Remotli {
    
    // provide a convenience initializer if you want to encapsulate the url for your host
    // within this class (not required).
    // if you do this you can initialize simply by declaring
    //
    // let myApi = MySiteAPI()
    //
    convenience init() {
        self.init(host: "my-host-api.com")
    }

    // Sets up an additional basic auth plugin with username/password.
    // This will configure all outgoing requests to this remote host with an
    // Authentication header set with a basic auth token value.
    override func setup() {
        addRequestPlugin(BasicAuthPlugin.init(username: "my-basic-auth-username", password: "secret-password"))
    }

    // Fetches the token from this remote host, making use of a route initializer and
    // specifying that an anonymous token is being requested.
    func getToken(completion: @escaping (Token?, Error?) -> Void) {
        send(requestFor: TokenRoute.anonymous, completion: completion)
    }

    // Logs in to this remote host and returns a User model object if successful.
    // Makes use of a route initializer for convenience.
    func login(username: String, password: String, completion: @escaping (User?, Error?) -> Void) {
        let route = UserRoute.login(username: username, password: password)
        send(requestFor: route, completion: completion)
    }
}

支持多个远程服务

如果你的应用程序需要访问多个远程Web服务,创建一个用于封装远程主机配置设置的工厂类可能非常有用。

实现这一点的途径之一是以下操作。

// The default scheme configuration setting is .https.
class RemoteSites {

    // Defines a remote host with an explicit .https scheme set as an example, if not specified https is set by default.
    static func google() -> Configuration {
        return Configuration(scheme: .https, host: "google.com")
    }

    // Defines a second remote host.
    static func flickr() -> Configuration {
        return Configuration(host: "flickr.com")
    }
}

// then in the class where your network access is handled you could setup your remote clients like this
internal var google: GoogleClient
internal var flickr: FlickrClient

init() {
    setupRemoteSites()
}

func setupRemoteSites() {

    // setup and configure custom url session for google service
    let urlConfig = URLSessionConfiguration.init()
    urlConfig.allowsCellularAccess = true
    urlConfig.httpCookieAcceptPolicy = .always
    let urlSession = URLSession.init(configuration: urlConfig)
    
    // setup google with the custom url session
    google = Remotli(configuration: RemoteSites.google(), session: urlSession, useReachability: false)
    
    // setup flickr with URLSession.shared and useReachability = true by default
    flickr = Remotli(configuration: RemoteSites.flickr())
}

计划特性

  • 支持XML
  • 更大范围的配置选项
  • 支持自定义错误类型

依赖项

要求

  • iOS 10.0+
  • Xcode 9
  • Swift 4
  • [CocoaPods]