? ? ? ? 本文演示的樣例效果同前文是一樣的汞斧,都是做一個(gè) GitHub
資源搜索功能汇在。只不過前面 ViewModel
里的輸入輸出使用是普通的 Observable
序列,這次我們改用 Driver
這個(gè)特征序列褥芒。
四吧恃、一個(gè)使用 Driver 的 MVVM 樣例
1,效果圖
(1)當(dāng)我們?cè)诒砀裆戏降乃阉骺蛑休斎胛淖謺r(shí)翠霍,會(huì)實(shí)時(shí)地去請(qǐng)求 GitHub
接口查詢所有匹配的資源庫锭吨。
(2)數(shù)據(jù)返回后會(huì)將查詢結(jié)果數(shù)量顯示在導(dǎo)航欄標(biāo)題上,同時(shí)把最匹配的資源條目顯示顯示在表格中(這個(gè)是 GitHub
接口限制寒匙,由于數(shù)據(jù)太多零如,可能不會(huì)一次全部都返回)。
(3)點(diǎn)擊某個(gè)單元格锄弱,會(huì)彈出顯示該資源的詳細(xì)信息(全名和描述)
(4)刪除搜索框的文字后考蕾,表格內(nèi)容同步清空,導(dǎo)航欄標(biāo)題變成顯示“hangge.com
”
2会宪,準(zhǔn)備工作
(1)首先我們?cè)陧?xiàng)目中配置好 RxSwift
肖卧、Alamofire
、Moya
掸鹅、Result
這幾個(gè)庫塞帐,具體步驟可以參考這篇文章:
(2)為了方便將結(jié)果映射成自定義對(duì)象沟沙,我們還需要引入 ObjectMapper、Moya-ObjectMapper 這兩個(gè)第三方庫壁榕。具體步驟可以參考這篇文章:
3赎瞎,樣例代碼
(1)我們先創(chuàng)建一個(gè) GitHubAPI.swift
文件作為網(wǎng)絡(luò)請(qǐng)求層牌里,里面的內(nèi)容如下(這個(gè)同前文一樣):
- 首先定義一個(gè)
provider
,即請(qǐng)求發(fā)起對(duì)象务甥。往后我們?nèi)绻l(fā)起網(wǎng)絡(luò)請(qǐng)求就使用這個(gè)provider
牡辽。 - 接著聲明一個(gè)
enum
來對(duì)請(qǐng)求進(jìn)行明確分類,這里我們只有一個(gè)枚舉值表示查詢資源敞临。 - 最后讓這個(gè)
enum
實(shí)現(xiàn)TargetType
協(xié)議态辛,在這里面定義我們各個(gè)請(qǐng)求的url
、參數(shù)挺尿、header
等信息奏黑。
import Foundation
import Moya
import RxMoya
//初始化GitHub請(qǐng)求的provider
let GitHubProvider = MoyaProvider<GitHubAPI>()
/** 下面定義GitHub請(qǐng)求的endpoints(供provider使用)**/
//請(qǐng)求分類
public enum GitHubAPI {
case repositories(String) //查詢資源庫
}
//請(qǐng)求配置
extension GitHubAPI: TargetType {
//服務(wù)器地址
public var baseURL: URL {
return URL(string: "https://api.github.com")!
}
//各個(gè)請(qǐng)求的具體路徑
public var path: String {
switch self {
case .repositories:
return "/search/repositories"
}
}
//請(qǐng)求類型
public var method: Moya.Method {
return .get
}
//請(qǐng)求任務(wù)事件(這里附帶上參數(shù))
public var task: Task {
print("發(fā)起請(qǐng)求。")
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
}
}
//是否執(zhí)行Alamofire驗(yàn)證
public var validate: Bool {
return false
}
//這個(gè)就是做單元測試模擬的數(shù)據(jù)编矾,只會(huì)在單元測試文件中有作用
public var sampleData: Data {
return "{}".data(using: String.Encoding.utf8)!
}
//請(qǐng)求頭
public var headers: [String: String]? {
return nil
}
}
(2)接著定義好相關(guān)模型:GitHubModel.swift
(這個(gè)還是同前文一樣)
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"]
}
}
//單個(gè)倉庫模型
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)下面就是本文的重頭戲了熟史。我們創(chuàng)建一個(gè) ViewModel
,它的作用就是將用戶各種輸入行為窄俏,轉(zhuǎn)換成輸出狀態(tài)蹂匹。和前文不同的是,本樣例中不管輸入還是輸出都是 Driver
類型凹蜈。
關(guān)于 Driver
的優(yōu)點(diǎn)可以參考這篇文章:Swift - RxSwift的使用詳解18(特征序列2:Driver)
import Foundation
import RxSwift
import RxCocoa
class ViewModel {
/**** 輸入部分 ***/
//查詢行為
fileprivate let searchAction:Driver<String>
/**** 輸出部分 ***/
//所有的查詢結(jié)果
let searchResult: Driver<GitHubRepositories>
//查詢結(jié)果里的資源列表
let repositories: Driver<[GitHubRepository]>
//清空結(jié)果動(dòng)作
let cleanResult: Driver<Void>
//導(dǎo)航欄標(biāo)題
let navigationTitle: Driver<String>
//ViewModel初始化(根據(jù)輸入實(shí)現(xiàn)對(duì)應(yīng)的輸出)
init(searchAction:Driver<String>) {
self.searchAction = searchAction
//生成查詢結(jié)果序列
self.searchResult = searchAction
.filter { !$0.isEmpty } //如果輸入為空則不發(fā)送請(qǐng)求了
.flatMapLatest{
GitHubProvider.rx.request(.repositories($0))
.filterSuccessfulStatusCodes()
.mapObject(GitHubRepositories.self)
.asDriver(onErrorDriveWith: Driver.empty())
}
//生成清空結(jié)果動(dòng)作序列
self.cleanResult = searchAction.filter{ $0.isEmpty }.map{ _ in Void() }
//生成查詢結(jié)果里的資源列表序列(如果查詢到結(jié)果則返回結(jié)果限寞,如果是清空數(shù)據(jù)則返回空數(shù)組)
self.repositories = Driver.merge(
searchResult.map{ $0.items },
cleanResult.map{[]}
)
//生成導(dǎo)航欄標(biāo)題序列(如果查詢到結(jié)果則返回?cái)?shù)量,如果是清空數(shù)據(jù)則返回默認(rèn)標(biāo)題)
self.navigationTitle = Driver.merge(
searchResult.map{ "共有 \($0.totalCount!) 個(gè)結(jié)果" },
cleanResult.map{ "hangge.com" }
)
}
}
(4)最后我們視圖控制器(ViewController
)只需要調(diào)用 ViewModel
進(jìn)行數(shù)據(jù)綁定就可以了仰坦⌒┘海可以看到由于網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)處理等邏輯已經(jīng)被剝離到 ViewModel
中登失,VC
這邊的負(fù)擔(dān)大大減輕了莺褒。
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
//顯示資源列表的tableView
var tableView:UITableView!
//搜索欄
var searchBar:UISearchBar!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
//創(chuàng)建表視圖
self.tableView = UITableView(frame:self.view.frame, style:.plain)
//創(chuàng)建一個(gè)重用的單元格
self.tableView!.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
self.view.addSubview(self.tableView!)
//創(chuàng)建表頭的搜索欄
self.searchBar = UISearchBar(frame: CGRect(x: 0, y: 0,
width: self.view.bounds.size.width, height: 56))
self.tableView.tableHeaderView = self.searchBar
//查詢條件輸入
let searchAction = searchBar.rx.text.orEmpty.asDriver()
.throttle(0.5) //只有間隔超過0.5k秒才發(fā)送
.distinctUntilChanged()
//初始化ViewModel
let viewModel = ViewModel(searchAction: searchAction)
//綁定導(dǎo)航欄標(biāo)題數(shù)據(jù)
viewModel.navigationTitle.drive(self.navigationItem.rx.title).disposed(by: disposeBag)
//將數(shù)據(jù)綁定到表格
viewModel.repositories.drive(tableView.rx.items) { (tableView, row, element) in
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
cell.textLabel?.text = element.name
cell.detailTextLabel?.text = element.htmlUrl
return cell
}.disposed(by: disposeBag)
//單元格點(diǎn)擊
tableView.rx.modelSelected(GitHubRepository.self)
.subscribe(onNext: {[weak self] item in
//顯示資源信息(完整名稱和描述信息)
self?.showAlert(title: item.fullName, message: item.description)
}).disposed(by: disposeBag)
}
//顯示消息
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()
}
}
功能改進(jìn):將網(wǎng)絡(luò)請(qǐng)求服務(wù)提取出來
(1)從上面的樣例可以發(fā)現(xiàn),我們?cè)?ViewModel
中是直接調(diào)用 Moya
的 Provider
進(jìn)行數(shù)據(jù)請(qǐng)求传泊,并進(jìn)行模型轉(zhuǎn)換鼠渺。
(2)我們也可以把網(wǎng)絡(luò)請(qǐng)求和數(shù)據(jù)轉(zhuǎn)換相關(guān)代碼提取出來,作為一個(gè)專門的 Service
眷细。比如 GitHubNetworkService
拦盹,內(nèi)容如下:
import RxSwift
import RxCocoa
import ObjectMapper
class GitHubNetworkService {
//搜索資源數(shù)據(jù)
func searchRepositories(query:String) -> Driver<GitHubRepositories> {
return GitHubProvider.rx.request(.repositories(query))
.filterSuccessfulStatusCodes()
.mapObject(GitHubRepositories.self)
.asDriver(onErrorDriveWith: Driver.empty())
}
}
(3)ViewModel
這邊不再直接調(diào)用 provider
,而是通過這個(gè) Service
就獲取需要的數(shù)據(jù)溪椎∑沼撸可以看到代碼簡潔許多:
import Foundation
import RxSwift
import RxCocoa
class ViewModel {
/**** 數(shù)據(jù)請(qǐng)求服務(wù) ***/
let networkService = GitHubNetworkService()
/**** 輸入部分 ***/
//查詢行為
fileprivate let searchAction:Driver<String>
/**** 輸出部分 ***/
//所有的查詢結(jié)果
let searchResult: Driver<GitHubRepositories>
//查詢結(jié)果里的資源列表
let repositories: Driver<[GitHubRepository]>
//清空結(jié)果動(dòng)作
let cleanResult: Driver<Void>
//導(dǎo)航欄標(biāo)題
let navigationTitle: Driver<String>
//ViewModel初始化(根據(jù)輸入實(shí)現(xiàn)對(duì)應(yīng)的輸出)
init(searchAction:Driver<String>) {
self.searchAction = searchAction
//生成查詢結(jié)果序列
self.searchResult = searchAction
.filter { !$0.isEmpty } //如果輸入為空則不發(fā)送請(qǐng)求了
.flatMapLatest(networkService.searchRepositories)
//生成清空結(jié)果動(dòng)作序列
self.cleanResult = searchAction.filter{ $0.isEmpty }.map{ _ in Void() }
//生成查詢結(jié)果里的資源列表序列(如果查詢到結(jié)果則返回結(jié)果恬口,如果是清空數(shù)據(jù)則返回空數(shù)組)
self.repositories = Driver.merge(
searchResult.map{ $0.items },
cleanResult.map{[]}
)
//生成導(dǎo)航欄標(biāo)題序列(如果查詢到結(jié)果則返回?cái)?shù)量,如果是清空數(shù)據(jù)則返回默認(rèn)標(biāo)題)
self.navigationTitle = Driver.merge(
searchResult.map{ "共有 \($0.totalCount!) 個(gè)結(jié)果" },
cleanResult.map{ "hangge.com" }
)
}
}