测试测试过 | ✓ |
Lang语言 | SwiftSwift |
许可证 | MIT |
发布最近发布 | 2017年10月 |
SwiftSwift 版本 | 4.0 |
SPM支持 SPM | ✗ |
由 Mauricio Cousillas,Diego Ernst,Martin Barreto,Karina XL 维护。他们的主页链接分别为 Mauricio Cousillas,Diego Ernst,Martin Barreto,Karina XL。
依赖 | |
Alamofire | ~> 4.5.1 |
RxSwift | = 4.0.0 |
RxCocoa | = 4.0.0 |
用
用 Swift 编写的基于协议的网络抽象层。深受 RxPagination 项目的启发,但基于 Alamofire 和您选择的 JSON 解析库。
RouteType
一致性提供 API 抽象。PaginationRequestType
一致性支持分页。OperaDecodable
协议一致性。OperaError
类型进行网络错误抽象。OperaSwift 的 OperaError
表示 NSURLSession
错误、Alamofire
错误或您的 JSON 解析库错误。Alamofire.Request
周 RxSwift 包装器返回 JSON 序列化类型的 Single
或数组。当发生错误事件时传递网络错误。PaginationRequestType
的 RxSwift 包装器返回包含序列化元素和有关当前、下一页和前一页信息的 PaginationResponseType
的 Single
。RouteType.sampleData
轻松模拟服务。RequestAdapters
通过 CompositeAdapter
。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
协议自行定制。
为了避免在每个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实现RouteType
的method
属性。
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)
只有当该RouteType
是MultipartRouteType
时,我们还可以连接一个上传进度处理程序
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
}
由于OperaDecodable
和Decodable.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通过遵循URLRequestConvertible
的PaginationRequestType
协议表示分页请求,通常我们不需要创建一个新的类型来符合它。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
在参加贡献前,请查看 CONTRIBUTING 文件以获取更多信息。
如果您的应用中使用了 Opera,我们非常希望听到您的反馈!请在 twitter 上联系我们。
按照以下 4 个步骤运行示例项目
通过使它们中的一个采用 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 的默认设置如下
可以在 CHANGELOG.md 文件中找到。