OperaSwift 3.0.1

OperaSwift 3.0.1

测试测试过
Lang语言 SwiftSwift
许可证 MIT
发布最近发布2017年10月
SwiftSwift 版本4.0
SPM支持 SPM

由 Mauricio Cousillas,Diego Ernst,Martin Barreto,Karina XL 维护。他们的主页链接分别为 Mauricio CousillasDiego ErnstMartin BarretoKarina XL



 
依赖
Alamofire~> 4.5.1
RxSwift= 4.0.0
RxCocoa= 4.0.0
 

❤️XMARTLABS 制作。查看我们所有的 开源贡献

简介

用 Swift 编写的基于协议的网络抽象层。深受 RxPagination 项目的启发,但基于 Alamofire 和您选择的 JSON 解析库。

功能

  • 通过 RouteType 一致性提供 API 抽象。
  • 通过 PaginationRequestType 一致性支持分页。
  • 支持任何 JSON 解析库,如 DecodableArgo,通过 OperaDecodable 协议一致性。
  • 通过 OperaError 类型进行网络错误抽象。OperaSwift 的 OperaError 表示 NSURLSession 错误、Alamofire 错误或您的 JSON 解析库错误。
  • 使用 Alamofire.Request 周 RxSwift 包装器返回 JSON 序列化类型的 Single 或数组。当发生错误事件时传递网络错误。
  • 使用 PaginationRequestType 的 RxSwift 包装器返回包含序列化元素和有关当前、下一页和前一页信息的 PaginationResponseTypeSingle
  • 能够通过 RouteType.sampleData 轻松模拟服务。
  • 能够使用多个 RequestAdapters 通过 CompositeAdapter
  • 可以使用 HTTP multipart 请求轻松上传文件或图像。
  • 每个 RouteType 的下载进度和在 MultipartRouteType 上的上传进度。

用法

路由设置

RouteType 是 REST API 端点的请求的高级表示。通过采用 RouteType 协议,类型能够创建其相应的请求。

import Alamofire
import OperaSwift

// just a hierarchy structure to organize routes
struct GithubAPI {
    struct Repository {}
}

extension GithubAPI.Repository {

  struct Search: RouteType {

      var method: HTTPMethod { return .get }
      var path: String { return "search/repositories" }
  }

  struct GetInfo: RouteType {

      let owner: String
      let repo: String

      var method: HTTPMethod { return .get }
      var path: String { return "repos/\(owner)/\(repo)" }
  }
}

或者,您可以选择从枚举符合 RouteType,其中每个枚举值都是一个特定的路由(API 端点)及其关联值。

如果您想了解更多,请查看 RouteType 协议定义的其余部分。

如您所见,任何符合 RouteType 的类型必须提供 baseUrl 和 Alamofire 的 manager 实例。

通常,这些值在我们的路由之间不会改变,因此我们可以通过在RouteType上实现一个协议扩展来提供它们,如下所示。

extension RouteType {

    var baseURL: URL {
        return URL(string: "https://api.github.com")!
    }

    var manager: ManagerType {
        return Manager.singleton
    }
}

现在,默认情况下,我们定义的所有RouteType都将提供https://api.github.com作为baseUrl,以及Manager.singleton作为mananger。您可以根据特定的RouteType协议自行定制。

默认RouteTypes

为了避免在每个RouteType中实现method属性,Opera为每个HTTPMethod提供了一种协议,这样您可以实施那些

protocol GetRouteType: RouteType {}
protocol PostRouteType: RouteType {}
protocol OptionsRouteType: RouteType {}
protocol HeadRouteType: RouteType {}
protocol PutRouteType: RouteType {}
protocol PatchRouteType: RouteType {}
protocol DeleteRouteType: RouteType {}
protocol TraceRouteType: RouteType {}
protocol ConnectRouteType: RouteType {}

它们相当简单,它们只是使用与之匹配的HTTPMethod实现RouteTypemethod属性。

额外的RouteTypes

ImageUploadRouteType

struct Upload: ImageUploadRouteType {

  let image: UIImage
  let encoding: ImageUploadEncoding = .jpeg(quality: 0.80)
  let path = "/upload"
  let baseURL = URL(string: "...")!

}

然后像这样使用它

Upload(image: UIImage(named: "myImage")!)
  .rx
  .completable()
  .subscribe(
    onCompleted: {
      // success :)
    },
    onError: { error in
      // do something when something went wrong
    }
  )
  .addDisposableTo(disposeBag)

注意:如果您想通过
HTTP多部分请求上传文件列表,请使用MultipartRouteType

创建请求

现在我们可以轻松地创建一个Alamofire请求

let request: Request =  GithubAPI.Repository.GetInfo(owner: "xmartlabs", repo: "Opera").request

请注意,RouteType遵循Alamofire.URLConvertible,因此我们可以通过manager创建相关的Request

我们还可以利用Opera提供的响应式辅助工具

request
  .rx.collection()
  .subscribe(
    onNext: { (repositories: [Repository]) in
      // do something when networking and Json parsing completes successfully
    },
    onError: {(error: Error) in
      // do something when something went wrong
    }
  )
  .addDisposableTo(disposeBag)
getInfoRequest
  .rx.collection()
  .subscribe(
    onSuccess: { (repositories: [Repository]) in
      // do something when networking and Json parsing completes successfully
    },
    onError: {(error: Error) in
      guard let error = error as? OperaError else {
          //do something when it's not an OperaError
      }
      // do something with the OperaError
    }
  )
  .addDisposableTo(disposeBag)

如果您不打算将JSON响应解码到Model中,可以调用request.rx.any(),它返回一个当前请求的Single Any,并将在结果序列中传播OperaError错误(如果发生错误)。

错误处理

如果您使用的是响应式辅助工具(顺便说一句,它们很棒!)您可以在onError回调处处理错误,该回调返回一个Error,在网络或解析问题情况下,可以将其转换为OperaError以更容易使用。
OperaError封装了任何与网络或解析相关的错误。请注意,您必须在使用之前将onError回调上的Error进行强制类型转换。
OperaError还提供了一系列使访问错误数据更简单的属性

    public var error: Error
    public var request: URLRequest?
    public var response: HTTPURLResponse?
    public var body: Any?
    public var statusCode: Int?
    public var localizedDescription: String

示例

getInfoRequest
  .rx.object()
  .subscribe(
    onError: {(error: Error) in
      guard let error = error as? OperaError else {
          //do something when it's not an OperaError
      }
      // do something with the OperaError
      debugPrint("Request failed with status code \(error.statusCode)")
    }
  )
  .addDisposableTo(disposeBag)

下载 & 上传进度

每个RouteType都可以通过其响应式扩展可选地连接一个下载进度处理程序

let request: RouteType = ...
request
  .rx.collection()
  .downloadProgress {
    debugPrint("Download progress: \($0.fractionCompleted)")
  }
  .subscribe(
    onNext: { (repositories: [Repository]) in
      // do something when networking and Json parsing completes successfully
    },
    onError: {(error: Error) in
      // do something when something went wrong
    }
  )
  .addDisposableTo(disposeBag)

只有当该RouteTypeMultipartRouteType时,我们还可以连接一个上传进度处理程序

ImageUploadRouteType是一个用于轻松上传图片的特定MultipartRouteType

  let imageUpload: ImageUploadRouteType = ...
  imageUpload
    .rx
    .uploadProgress {
      debugPrint("Upload progress: \($0.fractionCompleted)")
    }
    .downloadProgress {
      debugPrint("Download progress: \($0.fractionCompleted)")
    }
    .completable()
    .subscribe(
      onCompleted: {
        debugPrint("Completed")
      },
      onError: { error in
        ...
      }
    )
    .addDisposableTo(disposeBag)

解码

我们说过Opera能够使用您喜欢的JSON解析库将JSON响应解码到Model中。让我们看看Opera是如何做到这一点的。

我们在3月16日以来一直在Xmartlabs使用Decodable作为我们的JSON解析库。在那之前,我们使用了Argo、ObjectMapper和其他许多库。我不想深入探讨我们选择JSON解析库的原因(我们确实有我们的理由;)但在Opera的实施/设计中,我们认为这是一个好的特性,能够对此进行灵活性设计。

这是我们的Repository模型...

struct Repository {

    let id: Int
    let name: String
    let desc: String?
    let company: String?
    let language: String?
    let openIssues: Int
    let stargazersCount: Int
    let forksCount: Int
    let url: NSURL
    let createdAt: NSDate

}

以及OperaDecodable协议

public protocol OperaDecodable {
    static func decode(_ json: Any) throws -> Self
}

由于OperaDecodableDecodable.Decodable要求我们实现相同的方法,我们只需要声明协议的一致性。

// Make Repository conforms to Decodable.Decodable
extension Repository: Decodable {

    static func decode(j: Any) throws -> Repository {
        return try Repository.init(  id: j => "id",
                                   name: j => "name",
                                   desc: j =>? "description",
                                company: j =>? ["owner", "login"],
                               language: j =>? "language",
                             openIssues: j => "open_issues_count",
                        stargazersCount: j => "stargazers_count",
                             forksCount: j => "forks_count",
                                    url: j => "url",
                              createdAt: j => "created_at")
    }
}

// Declare OperaDecodable adoption
extension Repository : OperaDecodable {}

使用Argo要难一些,我们不仅需要实现OperaDecodable,还需要声明协议采用。这就是Swift语言协议扩展功能派上用场的地方....

extension Argo.Decodable where Self.DecodedType == Self, Self: OperaDecodable {
  static func decode(json: Any) throws -> Self {
    let decoded = decode(JSON.parse(json))
    switch decoded {
      case .Success(let value):
        return value
      case .Failure(let error):
        throw error
    }
  }
}

现在,我们可以通过简单声明OperaDecodable协议采用,使任何Argo.Decodable模型与OperaDecodable一致。

extension Repository : OperaDecodable {}

Opera可用于与RxAlamofire一起使用。

分页

Opera通过遵循URLRequestConvertiblePaginationRequestType协议表示分页请求,通常我们不需要创建一个新的类型来符合它。Opera提供了一个泛型类型PaginationRequest<Element: OperaDecodable>,可以在大多数场景中使用。

采用PaginationRequestType的要求之一是实现以下初始化器

init(route: RouteType, page: String?, query: String?, filter: FilterType?, collectionKeyPath: String?)

所以我们可以创建一个分页请求...

let paginationRequest: PaginationRequest<Repository> = PaginationRequest(route: GithubAPI.Repository.Search(), collectionKeyPath: "items")

根据 GitHub 仓库 API 文档“搜索仓库”中的描述,JSON 响应数组的键在 "items" 下,因此我们传递 ""items"" 作为 collectionKeyPath 参数。

PaginationRequestType 类将一个 RouteType 实例封装起来,并包含与分页相关的额外信息,例如查询字符串、页面、过滤器等。它还提供了一些辅助方法,可以从当前分页请求信息中更新查询字符串、页面或过滤器值来获取一个新的分页请求。

let firtPageRequest = paginatinRequest.routeWithPage("1").request
let filteredFirstPageRequest = firtPageRequest.routeWithQuery("Eureka").request

以前的辅助方法的一个变体是 public func routeWithFilter(filter: FilterType) -> Self

最后,让我们看看抽象类 PaginationViewModel,它允许我们以非常直接的方式列出/分页/排序过滤器可解码项。

import UIKit
import RxSwift
import RxCocoa
import Opera

class SearchRepositoriesController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
    @IBOutlet weak var searchBar: UISearchBar!
    lazy var viewModel: PaginationViewModel<PaginationRequest<Repository>> = {
        return PaginationViewModel(paginationRequest: PaginationRequest(route: GithubAPI.Repository.Search(), collectionKeyPath: "items"))
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        // set up views
        tableView.backgroundView = emptyStateLabel
        tableView.keyboardDismissMode = .OnDrag

        // on viewWill appear load pagination view model by emitting false (do not cancel pending
        // request if any) to view model `refreshTrigger` PublishSubject.
        // viewModel is subscribed to `refreshTrigger` observable and starts a new request.
        rx.sentMessage(#selector(SearchRepositoriesController.viewWillAppear(_:)))
            .skip(1)
            .map { _ in false }
            .bindTo(viewModel.refreshTrigger)
            .addDisposableTo(disposeBag)

        // make model view loads next page when reaches table view bottom...  
        tableView.rx.reachedBottom
            .bindTo(viewModel.loadNextPageTrigger)
            .addDisposableTo(disposeBag)

        // Updates activity indicator accordingly based on modelView `loading` variable.
        viewModel.loading
            .drive(activityIndicatorView.rx.animating)
            .addDisposableTo(disposeBag)

        // updates tableView observing viewModel `elements`, since github api only works
        // if a query string is present we show no items if the first page is being loading
        // or UISearchBar text is empty.
        // By doing that whenever the search criteria is updated we take away all the item
        // from the table view giving a sense of being fetching/searching the server.
        // Notice the strongly typed `Repository` type below.
        Driver.combineLatest(viewModel.elements.asDriver(), viewModel.firstPageLoading, searchBar.rx.text.asDriver()) { elements, loading, searchText in
                return loading || searchText.isEmpty ? [] : elements
            }
            .asDriver()
            .drive(tableView.rx.itemsWithCellIdentifier("Cell")) { _, repository: Repository, cell in
                cell.textLabel?.text = repository.name
                cell.detailTextLabel?.text = "🌟\(repository.stargazersCount)"
            }
            .addDisposableTo(disposeBag)

        // whenever search bar text is changed, wait for 1/4 sec of search bar inactivity
        // then update the `viewModel` pagination request type (will cancel any pending request).
        // We propagates query string by binding it to viewModel.queryTrigger.
        searchBar.rx.text
            .filter { !$0.isEmpty }
            .throttle(0.25, scheduler: MainScheduler.instance)
            .bindTo(viewModel.queryTrigger)
            .addDisposableTo(disposeBag)

        // handles view empty state.
        Driver.combineLatest(viewModel.emptyState, searchBar.rx.text.asDriver().throttle(0.25)) { $0 ||  $1.isEmpty }
            .driveNext { [weak self] state in
                self?.emptyStateLabel.hidden = !state
                self?.emptyStateLabel.text = (self?.searchBar.text?.isEmpty ?? true) ? "Enter text to search repositories" : "No repositories found"
            }
            .addDisposableTo(disposeBag)
    }

    private lazy var emptyStateLabel: UILabel = {
        let emptyStateLabel = UILabel()
        emptyStateLabel.text = ControllerConstants.NoTextMessage
        emptyStateLabel.textAlignment = .Center
        return emptyStateLabel
    }()
    private let disposeBag = DisposeBag()
}

如果您想继续使用传统的 Alamofire 方式发送请求,Opera 通过提供以下响应序列器使这变得简单。

extension Request {
    /**
         Generic response object serialization that returns a OperaDecodable instance.

         - parameter keyPath:           keyPath to look up JSON object to serialize. Ignore parameter or pass nil when JSON object is the JSON root item.
         - parameter completionHandler: A closure to be executed once the request has finished.

         - returns: The request.
         */
    public func responseObject<T : OperaDecodable>(keyPath: String? = default, completionHandler: Response<T, OperaError> -> Void) -> Self
    /**
         Generic response object serialization that returns an Array of OperaDecodable instances.

         - parameter collectionKeyPath: keyPath to look up JSON array to serialize. Ignore parameter or pass nil when JSON array is the JSON root item.
         - parameter completionHandler: A closure to be executed once the request has finished.

         - returns: The request.
         */
    public func responseCollection<T : OperaDecodable>(collectionKeyPath: String? = default, completionHandler: Response<[T], OperaError> -> Void) -> Self
    /**
         Generic response object serialization. Notice that Response Error type is NetworkError.

         - parameter completionHandler: A closure to be executed once the request has finished.

         - returns: The request.
         */
    public func responseAnyObject(completionHandler: Response<AnyObject, OperaError> -> Void) -> Self
}

复合适配器

Opera 提供了一种使用多个 RequestAdapter 适配请求的方法。类 CompositeAdapter 提供了一种设置将应用于请求的 RequestAdapter 管道的方式。

要使用它,您只需要创建一个 CompositeAdapter,将您的所有适配器添加到其中,并将其设置为 NetworkManager 的适配器。

示例

let adapter = CompositeAdapter()
adapter.append(adapter: KeychainAccessTokenAdapter())
adapter.append(adapter: LanguageAdapter())
manager.adapter = adapter

要求

  • iOS 9.0+ / Mac OS X 10.9+ / tvOS 9.0+ / watchOS 2.0+
  • Xcode 8+

参与

  • 如果您想 贡献代码,请随时 提交补丁请求
  • 如果您有 功能请求,请 打开问题
  • 如果您 发现错误需要帮助,请在提交问题之前先 查看旧问题、常见问题解答(FAQ) 以及 StackOverflow(《XLOpera》标签)上的线程

在参加贡献前,请查看 CONTRIBUTING 文件以获取更多信息。

如果您的应用中使用了 Opera,我们非常希望听到您的反馈!请在 twitter 上联系我们。

示例

按照以下 4 个步骤运行示例项目

  • 克隆 Opera 仓库,
  • 运行 build_dependencies.sh 脚本(您必须已安装 carthage)。可选地,您可以通过 --platform 参数指定要构建的平台 - iOS、tvOS、OSX。
  • 打开 Opera 工作空间
  • 运行 Example 项目。

作者

贡献者 & 维护者

常见问题解答(FAQ)

如何设置附加请求参数,直接在创建来自 RouteType 或 PaginationRequestType 的 Alamofire 请求之前?

通过使它们中的一个采用 URLRequestParametersSetup 协议。

/**
 *  By adopting URLRequestParametersSetup a RouteType or PaginationRequestType is able to make a final customization to request parameters dictionary before they are encoded.
 */
public protocol URLRequestParametersSetup {
    func urlRequestParametersSetup(urlRequest: NSMutableURLRequest, parameters: [String: AnyObject]?) -> [String: AnyObject]?
}
如何自定义 NSMutableURLRequest,而无法通过 RouteType 和 PaginationRouteType 采用?

您可以在管理器上设置 Alamofire RequestAdapter 来自定义请求。

如何自定义分页请求的默认参数名称和值?

您可以使 PaginationRequest 采用 PaginationRequestTypeSettings

/**
 *  By adopting PaginationRequestTypeSettings a PaginationRequestType is able to customize its default parameter names such as query, page and its first page value.
 */
public protocol PaginationRequestTypeSettings {

    var queryParameterName: String { get }
    var pageParameterName: String { get }
    var firstPageParameterValue: String { get }

}

Opera 的默认设置如下

  • "q" 用于查询
  • "page" 用于页面
  • "1" 用于 firstPageParameterValue

变更日志

可以在 CHANGELOG.md 文件中找到。