Swift - RxSwift的使用详解74(ReactorKit架构3:GitHub资源搜索样例)
作者:hangge | 2018-07-02 08:10
GitHub 资源搜索与展示这个功能我之前也做过,当时分别用的是 MVVM 架构(点击查看)、以及 RxFeedback 架构(点击查看)。下面我使用 ReactorKit 来对其进行重构。

(3)使用 ReactorKit 架构自然还要引入 ReactorKit 库,具体步骤可以参考我之前写的这篇文章:
(2)接着定义好相关模型:GitHubModel.swift(需要实现 ObjectMapper 的 Mappable 协议,并设置好成员对象与 JSON 属性的相互映射关系。这里代码与之前那篇文章一样,没有变化。)
(3)为了前台页面能根据不同的响应情况(成功或失败)进行不同的处理。我们在 Result.swift 中定义了结果类型枚举,以及失败类型枚举。
(4)同时我们把网络请求和数据转换相关代码提取出来,作为一个专门的 Service(GitHubNetworkService.swift),方便使用。
(6)下面就是本文的重点了,在这里我们定义一个反应器(ViewReactor.swift),作用是将 Action 转换成 State。
(7)主视图控制器(ViewController.swift)里的代码就很简单了,只需要将页面上的用户行为绑定到 reactor 里的 action 上,同时将 reactor 里的 state 绑定到页面 UI 控件上即可。
1,效果图
(1)当我们在表格上方的搜索框中输入文字时,会实时地去请求 GitHub 接口查询相匹配的资源库。
(2)数据返回后,将查询结果数量显示在导航栏标题上,同时把匹配度最高的资源条目显示显示在表格中(这个是 GitHub 接口限制,由于数据太多,可能不会一次全部都返回)。
(3)在搜索过程中在页面中央会显示一个旋转的活动指示器,数据返回后指示器自动消失。
(4)如果搜索请求失败,会弹出相关的错误信息。

(5)点击某个单元格,会弹出显示该资源的详细信息(全名和描述)
(6)删除搜索框的文字后,表格内容同步清空,导航栏标题变成显示“hangge.com”

2,准备工作
(1)首先我们在项目中配置好 RxSwift、Alamofire、Moya、Result 这几个库,具体步骤可以参考我之前写的这篇文章:
(2)为了方便将结果映射成自定义对象,我们还需要引入 ObjectMapper、Moya-ObjectMapper 这两个第三方库。具体步骤可以参考我之前写的这篇文章:
(3)使用 ReactorKit 架构自然还要引入 ReactorKit 库,具体步骤可以参考我之前写的这篇文章:
(4)网络活动指示器以及错误提示框我使用的是 MBProgressHUD,具体步骤可以参考我之前写的这篇文章:
3,样例代码
(1)我们先创建一个 GitHubAPI.swift 文件作为网络请求层,里面的内容如下(这里代码与之前那篇文章一样,没有变化):
- 首先定义一个 provider,即请求发起对象。往后我们如果要发起网络请求就使用这个 provider。
- 接着声明一个 enum 来对请求进行明确分类,这里我们只有一个枚举值表示查询资源。
- 最后让这个 enum 实现 TargetType 协议,在这里面定义我们各个请求的 url、参数、header 等信息。
import Foundation
import Moya
import RxMoya
//初始化GitHub请求的provider
let GitHubProvider = MoyaProvider<GitHubAPI>()
/** 下面定义GitHub请求的endpoints(供provider使用)**/
//请求分类
public enum GitHubAPI {
case repositories(String) //查询资源库
}
//请求配置
extension GitHubAPI: TargetType {
//服务器地址
public var baseURL: URL {
return URL(string: "https://api.github.com")!
}
//各个请求的具体路径
public var path: String {
switch self {
case .repositories:
return "/search/repositories"
}
}
//请求类型
public var method: Moya.Method {
return .get
}
//请求任务事件(这里附带上参数)
public var task: Task {
print("发起请求。")
switch self {
case .repositories(let query):
var params: [String: Any] = [:]
params["q"] = query
params["sort"] = "stars"
params["order"] = "desc"
return .requestParameters(parameters: params,
encoding: URLEncoding.default)
default:
return .requestPlain
}
}
//是否执行Alamofire验证
public var validate: Bool {
return false
}
//这个就是做单元测试模拟的数据,只会在单元测试文件中有作用
public var sampleData: Data {
return "{}".data(using: String.Encoding.utf8)!
}
//请求头
public var headers: [String: String]? {
return nil
}
}
(2)接着定义好相关模型:GitHubModel.swift(需要实现 ObjectMapper 的 Mappable 协议,并设置好成员对象与 JSON 属性的相互映射关系。这里代码与之前那篇文章一样,没有变化。)
import Foundation
import ObjectMapper
//包含查询返回的所有库模型
struct GitHubRepositories: Mappable {
var totalCount: Int!
var incompleteResults: Bool!
var items: [GitHubRepository]! //本次查询返回的所有仓库集合
init() {
print("init()")
totalCount = 0
incompleteResults = false
items = []
}
init?(map: Map) { }
// Mappable
mutating func mapping(map: Map) {
totalCount <- map["total_count"]
incompleteResults <- map["incomplete_results"]
items <- map["items"]
}
}
//单个仓库模型
struct GitHubRepository: Mappable {
var id: Int!
var name: String!
var fullName:String!
var htmlUrl:String!
var description:String!
init?(map: Map) { }
// Mappable
mutating func mapping(map: Map) {
id <- map["id"]
name <- map["name"]
fullName <- map["full_name"]
htmlUrl <- map["html_url"]
description <- map["description"]
}
}
(3)为了前台页面能根据不同的响应情况(成功或失败)进行不同的处理。我们在 Result.swift 中定义了结果类型枚举,以及失败类型枚举。
import Foundation
//响应结果枚举
enum Result<T, E: Error> {
case success(T) //成功(里面是返回的数据)
case failure(E) //失败(里面是错误原因)
}
//失败情况枚举
enum GitHubServiceError: Error {
case offline
case githubLimitReached
}
//失败枚举对应的错误信息
extension GitHubServiceError {
var displayMessage: String {
switch self {
case .offline:
return "网络链接失败!"
case .githubLimitReached:
return "请求太频繁,请稍后再试!"
}
}
}
(4)同时我们把网络请求和数据转换相关代码提取出来,作为一个专门的 Service(GitHubNetworkService.swift),方便使用。
import RxSwift
import RxCocoa
import ObjectMapper
typealias SearchRepositoriesResponse = Result<(GitHubRepositories), GitHubServiceError>
class GitHubNetworkService {
//搜索资源数据
func searchRepositories(query:String) -> Observable<SearchRepositoriesResponse> {
return GitHubProvider.rx.request(.repositories(query))
.filterSuccessfulStatusCodes()
.mapObject(GitHubRepositories.self)
.map{ .success($0) } //成功返回
.asObservable()
.catchError({ error in
print("发生错误:",error.localizedDescription)
//失败返回(GitHub接口对请求频率有限制,太频繁会被拒绝:403)
return Observable.of(.failure(.githubLimitReached))
})
}
}
(6)下面就是本文的重点了,在这里我们定义一个反应器(ViewReactor.swift),作用是将 Action 转换成 State。
import RxSwift
final class ViewReactor: Reactor {
//数据请求服务
let networkService = GitHubNetworkService()
//代表用户行为
enum Action {
case updateQuery(String?) //查询条件内容改变
}
//代表状态变化
enum Mutation {
case setQuery(String?) //设置查询条件文字
case setLoading(Bool) //设置搜索状态
case setRepos(SearchRepositoriesResponse) //设置搜索结果
}
//代表页面状态
struct State {
var query: String? //搜索文字
var loading: Bool = false //当前正在加载
var results: [GitHubRepository] = [] //搜索结果集
var lastError: GitHubServiceError? //错误信息
var totalCount: Int = 0 //搜索结果数
var title: String {
get {
if query == nil || query!.isEmpty {
return "hangge.com"
} else {
return "共有 \(totalCount) 个结果"
}
}
}
}
//初始页面状态
let initialState: State
init() {
self.initialState = State()
}
//实现 Action -> Mutation 的转换
func mutate(action: Action) -> Observable<Mutation> {
switch action {
//搜索文字内容变化
case let .updateQuery(query):
//如果查询条件为空则直接清空结果(不需要查询)
guard !query!.isEmpty else { return Observable.just(Mutation.setQuery(query)) }
//依次执行下面2个状态变化动作
return Observable.concat([
//设置搜索文字
Observable.just(Mutation.setQuery(query)),
//设置搜索状态
Observable.just(Mutation.setLoading(true)),
//设置搜索结果
networkService.searchRepositories(query: query!)
//如果查询条件再次变化,但上一次搜索还未完成,上一次搜索会自动取消
.takeUntil(self.action.filter(isUpdateQueryAction))
.map(Mutation.setRepos),
//设置搜索状态
Observable.just(Mutation.setLoading(false))
])
}
}
//实现 Mutation -> State 的转换
func reduce(state: State, mutation: Mutation) -> State {
//从旧状态那里复制一个新状态
var state = state
//根据状态变化设置响应的状态值
switch mutation {
//设置查询条件
case let .setQuery(query):
state.query = query
//查询条件为空则直接清空结果
if state.query!.isEmpty {
state.results = []
state.totalCount = 0
}
//设置搜索状态
case let .setLoading(value):
state.loading = value
//设置响应结果(成功)
case .setRepos(.success(let repositories)):
state.results = repositories.items
state.totalCount = repositories.totalCount
state.lastError = nil
//设置响应结果(失败)
case .setRepos(.failure(let error)):
state.results = []
state.totalCount = 0
state.lastError = error
}
//返回新状态
return state
}
//判断当前的 action 是否是查询条件改变
private func isUpdateQueryAction(_ action: Action) -> Bool {
if case .updateQuery = action {
return true
} else {
return false
}
}
}
(7)主视图控制器(ViewController.swift)里的代码就很简单了,只需要将页面上的用户行为绑定到 reactor 里的 action 上,同时将 reactor 里的 state 绑定到页面 UI 控件上即可。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController, StoryboardView {
//显示资源列表的tableView
var tableView:UITableView!
//搜索栏
var searchBar:UISearchBar!
//活动指示器
var loadingHUD:MBProgressHUD!
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
//创建表视图
self.tableView = UITableView(frame:self.view.frame, style:.plain)
//创建一个重用的单元格
self.tableView!.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
self.view.addSubview(self.tableView!)
//创建表头的搜索栏
self.searchBar = UISearchBar(frame: CGRect(x: 0, y: 0,
width: self.view.bounds.size.width, height: 56))
self.tableView.tableHeaderView = self.searchBar
//创建一个加载指示器
self.loadingHUD = MBProgressHUD.showAdded(to: self.view, animated: false)
//设置reactor,会自动触发bind()方法
self.reactor = ViewReactor()
//单元格点击
self.tableView.rx.modelSelected(GitHubRepository.self)
.subscribe(onNext: {[weak self] item in
//显示资源信息(完整名称和描述信息)
self?.showAlert(title: item.fullName, message: item.description)
}).disposed(by: disposeBag)
}
//处理绑定事件(该方法会在 self.reactor 变化时自动触发)
func bind(reactor: ViewReactor) {
//Action(实现 View -> Reactor 的绑定)
searchBar.rx.text.orEmpty.changed //搜索框文字改变事件
.throttle(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.map(Reactor.Action.updateQuery) //转换为 Action.updateQuery
.bind(to: reactor.action) //绑定到 reactor.action
.disposed(by: disposeBag)
// State(实现 Reactor -> View 的绑定)
reactor.state.map { $0.results } //得到最新搜索结果
.bind(to: tableView.rx.items)(self.configureCell) //绑定到表格上
.disposed(by: disposeBag)
reactor.state.map { $0.title } //得到最新标题文字
.bind(to: navigationItem.rx.title) //绑定到导航栏上
.disposed(by: disposeBag)
reactor.state.map { !$0.loading } //得到请求状态
.bind(to: loadingHUD.rx.isHidden) //绑定到加载指示器上
.disposed(by: disposeBag)
//错误信息显示
reactor.state.map{ $0.lastError }
.subscribe(onNext: { [weak self] error in
if let error = error, let view = self?.view {
print(error.displayMessage)
let hud = MBProgressHUD.showAdded(to: view, animated: true)
hud.mode = .text ////纯文本模式
hud.label.text = error.displayMessage
hud.removeFromSuperViewOnHide = true //隐藏时从父视图中移除
hud.hide(animated: true, afterDelay: 2) //2秒钟后自动隐藏
}
})
.disposed(by: disposeBag)
}
//单元格配置
func configureCell(tableView: UITableView, row: Int, item: GitHubRepository)
-> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
cell.textLabel?.text = item.name
cell.detailTextLabel?.text = item.htmlUrl
return cell
}
//显示消息
func showAlert(title:String, message:String){
let alertController = UIAlertController(title: title,
message: message, preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "确定", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
self.present(alertController, animated: true, completion: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
全部评论(0)