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

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

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

2,准备工作
(1)首先我们在项目中配置好 RxSwift、Alamofire、Moya、Result 这几个库,具体步骤可以参考我之前写的这篇文章:
(2)为了方便地将结果映射成自定义对象,我们还需要引入 ObjectMapper、Moya-ObjectMapper 这两个第三方库。具体步骤可以参考我之前写的这篇文章:
(3)使用 RxFeedback 架构自然还要引入 RxFeedback 库,具体步骤可以参考我之前写的这篇文章:
(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))
})
}
}
(5)主视图控制器(ViewController.swift)里的代码就是本文的重点了。与 MVVM 架构不同的是,我们不再需要定义 ViewModel,而是通过状态、事件、反馈循环来实现整个页面的业务逻辑分离。
import UIKit
import RxSwift
import RxCocoa
import RxFeedback
class ViewController: UIViewController {
//显示资源列表的tableView
var tableView:UITableView!
//搜索栏
var searchBar:UISearchBar!
let disposeBag = DisposeBag()
//状态
struct State {
var search: String? { //搜索的文字
didSet {
if search == nil || search!.isEmpty {
self.title = "hangge.com"
self.loading = false
self.results = []
return
}
self.loading = true
}
}
var title: String? //导航栏标题
var loading: Bool //当前正在加载
var results: [GitHubRepository] //搜索结果
var lastError: GitHubServiceError? //错误信息
//返回初始化状态
static var empty: State {
return State(search: nil, title: "hangge.com", loading: false,
results: [], lastError: nil)
}
}
//事件
enum Event {
case searchChanged(String)
case response(SearchRepositoriesResponse)
}
override func viewDidLoad() {
super.viewDidLoad()
/**** 数据请求服务 ***/
let networkService = GitHubNetworkService()
//创建表视图
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
//创建一个加载指示器
let loadingHUD = MBProgressHUD.showAdded(to: self.view, animated: false)
Driver.system(
initialState: State.empty,
reduce: { (state: State, event: Event) -> State in
switch event {
case .searchChanged(let search):
var result = state
result.search = search
result.lastError = nil
return result
case .response(.success(let repositories)):
var result = state
result.results = repositories.items
result.title = "共有 \(repositories.totalCount!) 个结果"
result.loading = false
result.lastError = nil
return result
case .response(.failure(let error)):
var result = state
result.loading = false
result.lastError = error
return result
}
},
feedback:
bind(self) { me, state in
let subscriptions = [
//搜索结果绑定
state.map { $0.results }
.drive(me.tableView.rx.items)(me.configureCell),
//导航栏标题绑定
state.map { $0.title }
.drive(me.navigationItem.rx.title),
//加载指示器状态绑定
state.map { !$0.loading }
.drive(loadingHUD.rx.isHidden),
//消息指示器状态绑定
state.map { $0.lastError }.drive(onNext: { error in
if let error = error{
print(error.displayMessage)
let hud = MBProgressHUD.showAdded(to: self.view,
animated: true)
hud.mode = .text ////纯文本模式
hud.label.text = error.displayMessage
hud.removeFromSuperViewOnHide = true //隐藏时从父视图中移除
hud.hide(animated: true, afterDelay: 2) //2秒钟后自动隐藏
}
})
]
let events: [Signal<Event>] = [
me.searchBar.rx.text.orEmpty
.changed
.throttle(1, scheduler: MainScheduler.instance)//间隔超1秒才发
.asSignal(onErrorRecover: { _ in .empty() })
.map(Event.searchChanged)
]
return Bindings(subscriptions: subscriptions, events: events)
},
react(query: { $0.search }, effects: { search in
if search.isEmpty {
return Signal.empty()
}
return networkService.searchRepositories(query: search)
.asSignal(onErrorJustReturn: .failure(.offline))
.map(Event.response)
})
)
.drive()
.disposed(by: disposeBag)
//单元格点击
tableView.rx.modelSelected(GitHubRepository.self)
.subscribe(onNext: {[weak self] item in
//显示资源信息(完整名称和描述信息)
self?.showAlert(title: item.fullName, message: item.description)
}).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()
}
}
附:代码的拆分优化
上面样例中我们把 RxFeedBack 架构用到的 state、event、feedback 等等都定义在 ViewController 中,这样功能一旦多些,ViewController 里的代码就会变得十分冗长,难以阅读。下面我对代码进行改成,拆分成各个独立的文件。
1,GitHubRepositories+State.swift
将 State、Event 以及相关的 reduce 都放在这里面。
import Foundation
//状态
struct GitHubRepositoriesState {
var search: String? { //搜索的文字
didSet {
if search == nil || search!.isEmpty {
self.title = "hangge.com"
self.loading = false
self.results = []
return
}
self.loading = true
}
}
var title: String? //导航栏标题
var loading: Bool //当前正在加载
var results: [GitHubRepository] //搜索结果
var lastError: GitHubServiceError? //错误信息
}
//事件
enum GitHubRepositoriesEvent {
case searchChanged(String) //搜索文字改变
case response(SearchRepositoriesResponse) //结果响应
}
extension GitHubRepositoriesState {
//返回初始化状态
static var empty: GitHubRepositoriesState {
return GitHubRepositoriesState(search: nil, title: "hangge.com",
loading: false, results: [], lastError: nil)
}
static func reduce(state: GitHubRepositoriesState, event: GitHubRepositoriesEvent)
-> GitHubRepositoriesState {
switch event {
case .searchChanged(let search):
var result = state
result.search = search
result.lastError = nil
return result
case .response(.success(let repositories)):
var result = state
result.results = repositories.items
result.title = "共有 \(repositories.totalCount!) 个结果"
result.loading = false
result.lastError = nil
return result
case .response(.failure(let error)):
var result = state
result.loading = false
result.lastError = error
return result
}
}
}
2,GitHubRepositories+Feedback.swift
这里面放置与 UI 无关的反馈。
import Foundation
import RxSwift
import RxCocoa
import RxFeedback
struct GitHubRepositoriesFeedback {
//验证用户名
static func searchRepositories(networkService: GitHubNetworkService)
-> (Driver<GitHubRepositoriesState>) -> Signal<GitHubRepositoriesEvent> {
let query:(GitHubRepositoriesState) -> String?
= { $0.search }
let effects:(String) -> Signal<GitHubRepositoriesEvent>
= { return networkService.searchRepositories(query: $0)
.asSignal(onErrorJustReturn: .failure(.offline))
.map(GitHubRepositoriesEvent.response) }
return react(query: query, effects: effects)
}
}
3,ViewController.swift
可以发现代码经过前面的提取之后,主视图控制器的代码变得简洁许多。
import UIKit
import RxSwift
import RxCocoa
import RxFeedback
class ViewController: UIViewController {
//显示资源列表的tableView
var tableView:UITableView!
//搜索栏
var searchBar:UISearchBar!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
/**** 数据请求服务 ***/
let networkService = GitHubNetworkService()
//创建表视图
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
//创建一个加载指示器
let loadingHUD = MBProgressHUD.showAdded(to: self.view, animated: false)
//UI绑定
let bindUI: (Driver<GitHubRepositoriesState>) -> Signal<GitHubRepositoriesEvent> =
bind(self) { me, state in
let subscriptions = [
//搜索结果绑定
state.map { $0.results }
.drive(me.tableView.rx.items)(me.configureCell),
//导航栏标题绑定
state.map { $0.title }
.drive(me.navigationItem.rx.title),
//加载指示器状态绑定
state.map { !$0.loading }
.drive(loadingHUD.rx.isHidden),
//消息指示器状态绑定
state.map { $0.lastError }.drive(onNext: { error in
if let error = error{
print(error.displayMessage)
let hud = MBProgressHUD.showAdded(to: self.view,
animated: true)
hud.mode = .text ////纯文本模式
hud.label.text = error.displayMessage
hud.removeFromSuperViewOnHide = true //隐藏时从父视图中移除
hud.hide(animated: true, afterDelay: 2) //2秒钟后自动隐藏
}
})
]
let events: [Signal<GitHubRepositoriesEvent>] = [
me.searchBar.rx.text.orEmpty
.changed
.throttle(1, scheduler: MainScheduler.instance)//间隔超1秒才发
.asSignal(onErrorRecover: { _ in .empty() })
.map(GitHubRepositoriesEvent.searchChanged)
]
return Bindings(subscriptions: subscriptions, events: events)
}
Driver.system(
initialState: GitHubRepositoriesState.empty,
reduce: GitHubRepositoriesState.reduce,
feedback:
//UI反馈
bindUI,
//非UI的自动反馈(资源搜索)
GitHubRepositoriesFeedback.searchRepositories(networkService:
networkService)
)
.drive()
.disposed(by: disposeBag)
//单元格点击
tableView.rx.modelSelected(GitHubRepository.self)
.subscribe(onNext: {[weak self] item in
//显示资源信息(完整名称和描述信息)
self?.showAlert(title: item.fullName, message: item.description)
}).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)