Swift - RxSwift的使用詳解53(MVVM架構(gòu)演示3:使用Driver樣例)

? ? ? ? 本文演示的樣例效果同前文是一樣的汞斧,都是做一個(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肖卧、AlamofireMoya掸鹅、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)用 MoyaProvider 進(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" }
        )
    }
}

RxSwift使用詳解系列
原文出自:www.hangge.com轉(zhuǎn)載請(qǐng)保留原文鏈接

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末沼侣,一起剝皮案震驚了整個(gè)濱河市祖能,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蛾洛,老刑警劉巖养铸,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異轧膘,居然都是意外死亡钞螟,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門谎碍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鳞滨,“玉大人,你說我怎么就攤上這事蟆淀≌玻” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵熔任,是天一觀的道長提岔。 經(jīng)常有香客問我,道長笋敞,這世上最難降的妖魔是什么碱蒙? 我笑而不...
    開封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮夯巷,結(jié)果婚禮上赛惩,老公的妹妹穿的比我還像新娘。我一直安慰自己趁餐,他們只是感情好喷兼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著后雷,像睡著了一般季惯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上臀突,一...
    開封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天勉抓,我揣著相機(jī)與錄音,去河邊找鬼候学。 笑死藕筋,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的梳码。 我是一名探鬼主播隐圾,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼伍掀,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了暇藏?” 一聲冷哼從身側(cè)響起蜜笤,我...
    開封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎盐碱,沒想到半個(gè)月后瘩例,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡甸各,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了焰坪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片趣倾。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖某饰,靈堂內(nèi)的尸體忽然破棺而出儒恋,到底是詐尸還是另有隱情,我是刑警寧澤黔漂,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布诫尽,位于F島的核電站,受9級(jí)特大地震影響炬守,放射性物質(zhì)發(fā)生泄漏牧嫉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一减途、第九天 我趴在偏房一處隱蔽的房頂上張望酣藻。 院中可真熱鬧,春花似錦鳍置、人聲如沸辽剧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怕轿。三九已至,卻和暖如春辟拷,著一層夾襖步出監(jiān)牢的瞬間撞羽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來泰國打工衫冻, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留放吩,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓羽杰,卻偏偏與公主長得像渡紫,于是被迫代替她去往敵國和親到推。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容