---> 上節 【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例項 下載參考
下一節將介紹rxcocoa的使用場景,以及實現這個股票app所用到的更多rxswift方法。