RxSwift|RxSwift by Examples #3 – Networking
RxSwift by Examples 分成 4 部分。以下是 PART 3 的学习笔记和翻译整理。原文在这里。
随着我们越来越深入函数式响应式编程,我们将谈一谈网络,并连接数据与 UI。
对于 Rx 有许多网络 extension,包括 RxAlamofire 和 Moya。在这个教程中我们使用 Moya。
Moya
Moya 是对你需要处理的所有网络事件的一个抽象层。使用这个类库我们将很容易连接 API,这个 extension 集成了 RxSwift 和 ModelMapper。
设置
为了设置 Moya,我们需要一个 Provider,它集成了 setup for stubbing, endpoint closure 等等(当我们做测试的时候会更多地涉及)。对于我们简单的示例不需要这些,所以当前我们只初始化 Provider 和 RxSwift。
我们要做的第二件事是设置 Endpoint - 一个包含可能的终端目标的 enum。我们创建一个 enum 遵循 TargetType。什么是 TargetType?这是一个协议,包含了 url,方法,任务(比如 request/upload/download),参数和参数encoding(url 的基础)。
还有一件事。最后要指定的参数叫做 sampleData。Moya 重度依赖测试。它将测试视为一等公民。
示例
我们将使用 github api 去获取指定的 repo 的 issues。为了复杂化一点,得到 repo 对象之后我们将检查它是否存在,然后进行链式请求,获取这个 repo 的 issues。然后把 json map 成对象。我们还需要小心error,重复的请求,滥用api等等。
别担心,大部分内容我们已经在这个系列的第一部分中覆盖了。在这里我们需要理解链式和错误处理,并且知道如何连接操作至 table view。
最终 Issue Tracker 将是这样:输入完整的 repo 名字(包含 repo 所有者和斜杠),比如 apple/swift, apple/cups, moya/moya 诸如此类。当 repo 找到(一个 url 请求),接着搜索这个 repo 的 issues(第二个 url 请求)。这就是主要目标。
首先创建一个项目并用 cocoapods 安装它。这次需要更多的 pods。我们将使用 RxSwfit, Moya, RxCocoa, RxOptional 和 Moya 为 RxSwift 做的拓展以及用来 map 对象的 ModelMapper。
platform :ios, '8.0'
use_frameworks!
target 'RxMoyaExample' do
pod 'RxCocoa', '~> 3.0.0'
pod 'Moya-ModelMapper/RxSwift', '~> 4.1.0'
pod 'RxOptional'
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_TESTABILITY'] = 'YES'
config.build_settings['SWIFT_VERSION'] = '3.0'
end
end
end
第1步 - Controller 和 Moya 设置 从 UI 开始,一个 UITableView 和 UISearchBar。非常简单。
我们需要一个 Controller 来管理所有东西。在创建架构之前我们尝试描述一下这个 controller。
controller 要做什么呢?它将获取 search bar 的数据,传递给 model,从 model 获取 issues 并传递给 table view。
创建 IssueListViewController.swift,引入 modules 并做基础设置:
import Moya
import Moya_ModelMapper
import UIKit
import RxCocoa
import RxSwift
class IssueListViewController: UIViewController {@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var searchBar: UISearchBar!override func viewDidLoad() {
super.viewDidLoad()
setupRx()
}func setupRx() {
}
}
已经准备好了 setupRx() 方法,我们将设置 binding。在此之前,先设置 Moya 的 Endpoint。回忆一下,前面说过需要两步:第一步是 Provider,第二步是 Endpoint。
创建 GithubEndpoint.swift,创建 enums,放入一些可能的 targets:
import Foundation
import Moya
enum GitHub {
case userProfile(username: String)
case repos(username: String)
case repo(fullName: String)
case issues(repositoryFullName: String)
}
但是之前说过要遵循 TargetType,然而这个只是 enum。没错,我们将制作一个 GitHub enum 的 extension,它将包含所有需要的属性。我们需要 7 个。除了 baseURL,path 和 task,我们还需要 method,它是.get, .post 等请求。还有 parameters 和 parametersEncoding,以及 sampleData。
ENUM
下面,创建 GitHub 的 extension,遵循 TargetType:
import Foundation
import Moya
private extension String {
var URLEscapedString: String {
return self.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlHostAllowed)!
}
}
enum GitHub {
case userProfile(username: String)
case repos(username: String)
case repo(fullName: String)
case issues(repositoryFullName: String)
}
extension GitHub: TargetType {
var baseURL: URL { return URL(string: "https://api.github.com")! }
var path: String {
switch self {
case .repos(let name):
return "/users/\(name.URLEscapedString)/repos"
case .userProfile(let name):
return "/users/\(name.URLEscapedString)"
case .repo(let name):
return "/repos/\(name)"
case .issues(let repositoryName):
return "/repos/\(repositoryName)/issues"
}
}
var method: Moya.Method {
return .get
}
var parameters: [String: Any]? {
return nil
}
var sampleData: Data {
switch self {
case .repos(_):
return "{{\"id\": \"1\", \"language\": \"Swift\", \"url\": \"https://api.github.com/repos/mjacko/Router\", \"name\": \"Router\"}}}".data(using: .utf8)!
case .userProfile(let name):
return "{\"login\": \"\(name)\", \"id\": 100}".data(using: .utf8)!
case .repo(_):
return "{\"id\": \"1\", \"language\": \"Swift\", \"url\": \"https://api.github.com/repos/mjacko/Router\", \"name\": \"Router\"}".data(using: .utf8)!
case .issues(_):
return "{\"id\": 132942471, \"number\": 405, \"title\": \"Updates example with fix to String extension by changing to Optional\", \"body\": \"Fix it pls.\"}".data(using: .utf8)!
}
}
var task: Task {
return .request
}
var parameterEncoding: ParameterEncoding {
return JSONEncoding.default
}
}
整个 GithubEndpooint.swift 都完成了。看起来似乎很可怕,但如果仔细阅读它其实并非如此。在这里我们不需要发送任何参数,所以返回 nil。在这个例子中 method 总是 .get。 baseURL 也是一样。只有 sampleData 和 path 需要放到 switch 中。
【RxSwift|RxSwift by Examples #3 – Networking】如果你需要添加其他目标,你可能需要看看它的请求是需要 .get 还是 .post 方法,可能还需要参数,那么你需要给它添加 switch。
我们还添加了 URLEscapedString 函数,当需要 encoding URL 中的字符时很有帮助。
Controller
回到 controller。现在要实现 Moya 的 Provider。还需要实现当点击 cell 时隐藏键盘,这些 RxSwift 都已经做好了。为此我们还需要 DisposeBag。此外我们将创建新的 Observable,它会是 search bar 中的 text,不过是过滤后的(移除重复,等待改变,与 part 1 一样)
总之,我们需要添加 3 个属性,实现 setupRx() 方法。
class IssueListViewController: UIViewController {
...
let disposeBag = DisposeBag()
var provider: RxMoyaProvider!
var latestRepositoryName: Observable {
return searchBar
.rx.text
.orEmpty
.debounce(0.5, scheduler: MainScheduler.instance)
.distinctUntilChanged()
}
...
func setupRx() {
// First part of the puzzle, create our Provider
provider = RxMoyaProvider()// Here we tell table view that if user clicks on a cell,
// and the keyboard is still visible, hide it
tableView
.rx.itemSelected
.subscribe(onNext: { indexPath in
if self.searchBar.isFirstResponder == true {
self.view.endEditing(true)
}
})
.addDisposableTo(disposeBag)
}
...
}
希望你觉得 latestRepositoryName 看起来很熟悉,因为在 part 1 深入讨论过了。接着看看更多有意思的东西。
首先我们设置了之前提到过的神秘的 Provider。如你所见,没有什么特别,只是 initializer。因为我们使用 Moya 和 RxSwift,所以必须使用 RxMoyaProvider。如果你想使用 Moya + ReactiveCocoa,或者只使用 Moya 来写 API,provider 会有些不同(纯 Moya 用MoyaProvider,ReactiveCocoa + Moya 用 ReactiveCocoaMoyaProvider)。
我们需要隐藏键盘。感谢 RxCocoa,我们可以访问 tableView.rx.itemSelected,每次当用户点击 table view cell 的时候它就会发出信号。当然我们可以订阅它,做我们要做的事(因此键盘)。我们检查了 search bar 是否是 first responder(如果键盘显示),于是隐藏它。
第2步 - Network model and mapping objects 现在我们需要 model 基于 text 提供数据给我们。不过首先,在发送任何信息之前需要先解析对象。感谢我们的朋友 ModelMapper 做了这个工作。我们需要两个 entity,一个给 repo,一个给 issue。这很容易创建,我们需要遵循 Mappable 协议,并用 try 解析对象。
RepositoryEntity.swift
import Mapper
struct Repository: Mappable {let identifier: Int
let language: String
let name: String
let fullName: Stringinit(map: Mapper) throws {
try identifier = map.from("id")
try language = map.from("language")
try name = map.from("name")
try fullName = map.from("full_name")
}
}
IssueEntity.swift
import Mapper
struct Issue: Mappable {let identifier: Int
let number: Int
let title: String
let body: Stringinit(map: Mapper) throws {
try identifier = map.from("id")
try number = map.from("number")
try title = map.from("title")
try body = map.from("body")
}
}
我们不需要更多属性,你可以根据 GitHub API 文档添加更多。
Networking Model
现在进入这个教程最有意思的部分。IssueTrackerModel,网络层的核心。
首先,我们的 model 将有 Provider 属性,我们通过 init 传递它。然后我们将有一个属性来观察 text,这是一个 Observable 类型,这是我们的资源的 repositoryNames,我们的 view controller 将会传递。我们需要一个方法返回 observable 序列,issue 数组,Observable,view controller 将用来绑定到 table view。我们不需要实现 init,因为 swift 原生支持 memberwise initializer。
创建 IssueTrackerModel.swift
import Foundation
import Moya
import Mapper
import Moya_ModelMapper
import RxOptional
import RxSwift
struct IssueTrackerModel {let provider: RxMoyaProvider
let repositoryName: Observablefunc trackIssues() -> Observable {}internal func findIssues(repository: Repository) -> Observable {
return repositoryName
.observeOn(MainScheduler.instance)
.flatMapLatest { name -> Observable in
print("Name: \(name)")
return self
.findRepository(name: name)
}
.flatMapLatest { repository -> Observable<[Issue]?> in
guard let repository = repository else { return Observable.just(nil) }print("Repository: \(repository.fullName)")
return self.findIssues(repository: repository)
}
.replaceNilWith([])
}
分步讲解:
- 我们想确认它在 MainScheduler 中观察,因为这个 model 的目标是绑定至 UI,在我们的示例中是 table view。
- 我们转换 text(repo 名)到 observable repo 序列,它可以是 nil,以防它不能正确地 map 对象。
- 检查 map 出的结果是否 nil。如果是 nil,下一个 flatMapLatest() 确保返回空数组。 Observable.just(nil) 意味着我们将发送一个元素作为 observable(在示例中这个元素是 nil)。如果不是 nil,我们想把它转换成 issue 数组(如果 repo 有 issue),它可以返回 nil 或者数组,所以仍然需要 observable 的 optional 数组。
- .replaceNilWith([]) 是 RxOptional extension,帮助我们处理 nil,在示例中我们把 nil 转换成空数组,清空 table view。
第3步 - 绑定 issue 到 table view 最后一步要连接 model 中的数据到 table view。这意味着我们需要绑定 observable 到 table view。
通常你要让 view controller 遵循 UITableViewDataSource,实现一些方法,比如 number of rows, cell for row 等等,然后指派 dataSource 给 view controller。
用 RxSwift,我们可以在一个闭包中设置 UITableViewDataSource。RxCocoa 提供另一个很棒的工具,叫做 rx.itemWithCellFactory,它在一个闭包中处理要显示的 cell。这同步做了所有的事情,基于 observable 和我们提供的 closure。
回到 IssueListViewController,实现完整的 setupRx() 方法:
class IssueListViewController: UIViewController {
...
var issueTrackerModel: IssueTrackerModel!
...
func setupRx() {
// First part of the puzzle, create our Provider
provider = RxMoyaProvider()// Now we will setup our model
issueTrackerModel = IssueTrackerModel(provider: provider, repositoryName: latestRepositoryName)// And bind issues to table view
// Here is where the magic happens, with only one binding
// we have filled up about 3 table view data source methods
issueTrackerModel
.trackIssues()
.bindTo(tableView.rx.items) { tableView, row, item in
let cell = tableView.dequeueReusableCell(withIdentifier: "issueCell", for: IndexPath(row: row, section: 0))
cell.textLabel?.text = item.titlereturn cell
}
.addDisposableTo(disposeBag)// Here we tell table view that if user clicks on a cell,
// and the keyboard is still visible, hide it
tableView
.rx.itemSelected
.subscribe(onNext: { indexPath in
if self.searchBar.isFirstResponder == true {
self.view.endEditing(true)
}
})
.addDisposableTo(disposeBag)
}
...
}
这里新增是,新的属性给 IssueTrackerModel(也在 setupRx() 中初始化)。新的绑定:从 model 的 trackIssues() 方法,到 rx.itemsWithCellFactory 属性。别忘了修改 dequeueReusableCell() 方法中的 cellIndentifier。
至此,所有要实现的都已经实现了。run
推荐阅读
- sublime|sublime text 3最新注册码
- 如何把opencv编译到matlab|如何把opencv编译到matlab,c – 为OpenCV编译MATLAB绑定
- 悲情的两小无猜––桐原亮司和雪穗
- docker|Docker--私有仓库
- Material|Material design - Components– Menus
- Comp 4510
- ASP.NET Web Forms – ArrayList 对象简介
- ASP.NET Web Forms – Button 控件简介
- XML DOM – 访问节点概述
- XML DOM – 属性和方法概述