【RxSwift 實踐系列 2/3】thinking in Rx- Create和Drive

lizyyy發表於2018-03-05

---> 上節 【RxSwift 實踐系列 1/3】為什麼使用RxSwift

RxSwift 是一種程式設計思想,不是一門語言,學習他最難的部分就是thinking in Reactive Programming :把所有事件當作一個 stream來思考。 它的目的是讓資料/事件流和非同步任務能夠更方便的序列化處理

如果把我們程式中每一個操作都看成一個事件: 比如一個TextFiled中的文字改變,一個按鈕的點選,或者一個網路請求結束等,每一個事件源就可以看成一個管道,也就是Sequence,比如一個TextFiled,當我們改變裡邊的文字的時候 這個時候TextFiled就會不斷的發出事件, 從他的Sequence中不斷的流出 我們只需要監聽這個Sequence,每流出一個事件就做相應的處理。同理UIButton也是一個Sequence 每點選一次就流出一個事件


接下來我們一步步的用thinking in Reactive Programming思想來實現一個實時展示股票資訊的app.
app需求:

1、準備工作:

2、網路請求準備

3、功能:輸入股票賬號,顯示相關股票

4、新增到關注列表,從網路獲取股票資訊

5、實時顯示關注列表的股票資料情況
複製程式碼

1、 準備工作

我們需要建立一個stock的專案,並使用Podfile管理第三方庫。 關於RxSwift的基本使用方法可以在github的RxSwift檢視。

Podfile的配置如下:

platform :ios, '11.0'
use_frameworks!
target 'stock' do
    pod 'RxSwift','~> 4.0'
    pod 'RxCocoa','~> 4.0' #把UI庫和rx結合
    pod 'FMDB', '~>2.6.2'  #sqlite資料庫
    pod 'SwiftyJSON'  #json處理
    pod 'Moya/RxSwift'  #網路請求
    pod 'ObjectMapper', '~> 3.1' #Json轉模型
    pod 'MJRefresh', :inhibit_warnings => true #下拉重新整理
    pod 'RxDataSources', '~> 3.0'  #幫助我們優雅的使用tableView的資料來源方法
    pod 'NSObject+Rx'     #為我們提供 rx_disposeBag
    pod 'Then'            #提供快速初始化的語法糖
    pod 'Reusable'        #幫助我們優雅的使用自定義cell和view,不再出現Optional
end
 
 #rx的debug使用
 post_install do |installer|
     installer.pods_project.targets.each do |target|
         if target.name == 'RxSwift'
             target.build_configurations.each do |config|
                 if config.name == 'Debug'
                     config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['-D', 'TRACE_RESOURCES']
                 end
             end
         end
     end
 end


複製程式碼

2、 網路請求

通過網路請求,處理返回資料
    
知識點:Observable  create   subscribe   disposed  moya
複製程式碼

thinking in Reactive Programming

網路請求是一個經典的非同步請求,moya是基於Alamofire實現的一個網路請求,它的網路請求也是一個可訂閱的stream,我們在此基礎上建立一個可訂閱的模型,把網路返回結果傳送給這個模型,提供給訂閱者展出到uitableview中

    
    /**
        這裡我們使用moya來做網路請求,moya的具體使用方法參見[Moya](https://github.com/Moya/Moya)
        我們把moya從網路api返回的結果當做一個stream,使用rxswift的create把返回結果變成一個可訂閱的資訊流
    */
    //create的方法可以參見手冊:
    create(_ subscribe: @escaping (RxSwift.AnyObserver<Self.E>) -> Disposable)
    //主要步驟是:
    Observable.create { observer in
        observer.onNext(10)
        observer.onCompleted()
        return  Disposables.create()
    }
        
    //接下來我們實現這個從api獲取股票資訊的方法,建立了一個可訂閱的資料模型
    let netToolProvider = MoyaProvider<StockApi>()
    func searchFromApi(repositoryName: String) -> Observable<[SearchModel]> {
        return Observable.create { observer in
            netToolProvider.rx.request(.SearchStocks(code:repositoryName))
                .mapJSON() //Moya RxSwift的擴充套件方法,可以把返回的資料解析成 JSON 格式
                .subscribe( //訂閱返回的結果
                    onSuccess:{json in
                        let info = self.parseSearchResponse(response: json as AnyObject) // 把返回解析成一個listmodel物件
                        observer.on(.next(info)) //傳送next 內容是listmodel
                        observer.on(.completed)  //傳送成功
                },
                    onError:{error in
                        observer.on(.error(error)) //傳送錯誤
                }).disposed(by: self.bag)//自動記憶體處理機制
            
            return  Disposables.create()
        }
    }
    
    private func parseSearchResponse(response: AnyObject) -> [SearchModel] {
        var info: [SearchModel] = []
        let items = response["stocks"] as! [[String:AnyObject]]
        let _ = items.filter{
                return $0["stock_id"] as! Int != 0 //把退市的去掉
            }.map{
                info.append(SearchModel(JSON: $0)!)
            }
        return info;
    }
    
複製程式碼

我們先把這個方法留著,等會我們會訂閱這個模型,展示到uitableview中去

3、響應輸入框將結果展示到列表

使用者輸入股票編號,呼叫介面去顯示搜尋結果
    
知識點:RxCocoa ,bind, drive
複製程式碼

thinking in Reactive Programming:

把輸入框事件當做一個流,使用者的輸入就會不斷的發出事件流,我們只需要監聽事件流就可以響應使用者的每次輸入。

searchFiled.rx.text
    .filter{
        ($0?.lengthOfBytes(using: .utf8))! > 0 //輸入長度大於1才處理資料
    }
    .throttle(0.5, scheduler: MainScheduler.instance) //延遲0.5秒再執行
    .subscribe{ //訂閱這個事件
        print($0) //列印使用者的輸入
}.disposed(by: rx.disposeBag)
複製程式碼

獲取使用者的輸入後,還需要即時的根據使用者輸入去api獲取搜尋結果,對於搜尋結果我們已經在剛才處理了。現在要做的是使用者輸入和api搜尋的事件繫結在一起。這裡rxswift提供了bind方法。

bind,這個方法意圖就是將一個被觀察者與一個指定的觀察者進行繫結,被觀察者事件流中發出的所有事件元素都會讓觀察者接收。
例如:
textField.rx_text
    .bindTo(label.rx_text)
    .addDisposableTo(disposeBag)
複製程式碼

接下來我們來處理事件流發出者(searchFiled)和觀察者(resultTableView)繫結在一起,當使用者輸入查詢字串,即時去介面獲取查詢結果,展示在uitableview中:

searchFiled.rx.text
    .filter{
        ($0?.lengthOfBytes(using: .utf8))! > 0 //長度大於1
    }
    .throttle(0.5, scheduler: MainScheduler.instance) //延遲0.5秒再執行
    .flatMap{
        ListViewModel().searchFromApi(repositoryName:String(describing: $0!)) //查詢結果,返回可訂閱的SearchModel
    }
    //事件流發出者(searchFiled)和觀察者(resultTableView)繫結在一起
    .bind(to: self.resultTableView.rx.items(cellIdentifier: "SearchTableViewCell", cellType: SearchTableViewCell.self)) {(_, model:SearchModel, cell:SearchTableViewCell) in
        cell.nameLabel.text = model.name
        cell.codeLabel.text = model.code
    }
.disposed(by: rx.disposeBag)

複製程式碼

但是如果用bind,會有幾個問題: 1.如果searchFromApi錯誤(連線失敗,引數錯誤等),錯誤將會丟失(得不到任何處理),UI也不會再處理和響應任何的結果

2.如果searchFromApi返回的結果不是在主執行緒,那麼不在子執行緒中對UI進行繫結,就會出現未知的錯誤

3.如果返回的結果繫結到了兩個UI上,那麼意味著每一個UI都要進行一次網路請求,即進行兩次網路請求,那麼,我們可以稍稍修改一下

我們可以這樣修改一下:

    searchFiled.rx.text
        .filter{
            ($0?.lengthOfBytes(using: .utf8))! > 0 //長度大於1
        }
        .throttle(0.5, scheduler: MainScheduler.instance) //延遲0.5秒再執行
        .flatMap{
            ListViewModel().searchFromApi(repositoryName:String(describing: $0!))//查詢結果,返回可訂閱的SearchModel
                .observeOn(MainScheduler.instance) // 將返回結果切換到到主執行緒上
                .catchErrorJustReturn([])  // 如果有問題,錯誤結果將會得到處理
        }
        .share(replay: 1)
        //事件流發出者(searchFiled)和觀察者(resultTableView)繫結在一起
        .bind(to: self.resultTableView.rx.items(cellIdentifier: "SearchTableViewCell", cellType: SearchTableViewCell.self)) {(_, model:SearchModel, cell:SearchTableViewCell) in
            cell.nameLabel.text = model.name
            cell.codeLabel.text = model.code
        }
    .disposed(by: rx.disposeBag)

複製程式碼

這裡插一句:subscribeOn和observeOn的區別: subscribeOn()設定起點在哪個執行緒,observeOn()是設定的後續工作在哪個執行緒。

這裡我們設計的還是不夠完美,rxswift中提供了drive方法,可是說是專為UI繫結量身打造

drive方法只能在Driver序列中使用,Driver有以下特點:

1. Driver序列不允許發出error,

2. Driver序列的監聽只會在主執行緒中。
複製程式碼

讓我們來看看drive的實現方法:

searchFiled.rx.text
    .filter{
        ($0?.lengthOfBytes(using: .utf8))! > 0 //長度大於1
    }
    .throttle(0.5, scheduler: MainScheduler.instance) //延遲0.5秒再執行
    .flatMap{
        ListViewModel().searchFromApi(repositoryName:String(describing: $0!))
    }
    .asDriver(onErrorJustReturn: [])
    .drive(resultTableView.rx.items(cellIdentifier: "SearchTableViewCell", cellType: SearchTableViewCell.self)) {
        (tableView, element, cell) in
        cell.nameLabel.text = element.name
        cell.codeLabel.text = element.code
    }
    .disposed(by: rx.disposeBag)

複製程式碼
所以以下情況你可以使用Driver替換BindTo: 

1、不能發出error;

2、在主執行緒中監聽;

3、共享事件流;
複製程式碼

本節先到這裡,詳細程式碼可以到這裡:rxStock例項 下載參考

app截圖

下一節將介紹rxcocoa的使用場景,以及實現這個股票app所用到的更多rxswift方法。

【RxSwift 實踐系列 3/3】thinking in Rx- UITableView

上一節回顧 【RxSwift 實踐系列 1/3】為什麼使用RxSwift

相關文章