最近剛剛把接手的OC專案搞定,經過深思熟慮後,本人決定下個專案起就使用Swift(學了這麼久的Swift還沒真正用到實際專案裡。。。),而恰巧RxSwift已經出來有一些時間了,語法也基本上穩定,遂隻身前來試探試探這RxSwift,接著就做了個小Demo,有興趣的同學可以瞧一瞧~
結構
.
├── Controller
│ └── LXFViewController.swift // 主檢視控制器
├── Extension
│ └── Response+ObjectMapper.swift // Response分類,Moya請求完進行Json轉模型或模型陣列
├── Model
│ └── LXFModel.swift // 模型
├── Protocol
│ └── LXFViewModelType.swift // 定義了模型協議
├── Tool
│ ├── LXFNetworkTool.swift // 封裝Moya請求
│ └── LXFProgressHUD.swift // 封裝的HUD
├── View
│ ├── LXFViewCell.swift // 自定義cell
│ └── LXFViewCell.xib // cell的xib檔案
└── ViewModel
└── LXFViewModel.swift // 檢視模型複製程式碼
第三方庫
RxSwift // 想玩RxSwift的必備庫
RxCocoa // 對 UIKit Foundation 進行 Rx 化
NSObject+Rx // 為我們提供 rx_disposeBag
Moya/RxSwift // 為RxSwift專用提供,對Alamofire進行封裝的一個網路請求庫
ObjectMapper // Json轉模型之必備良品
RxDataSources // 幫助我們優雅的使用tableView的資料來源方法
Then // 提供快速初始化的語法糖
Kingfisher // 圖片載入庫
SnapKit // 檢視約束庫
Reusable // 幫助我們優雅的使用自定義cell和view,不再出現Optional
MJRefresh // 上拉載入、下拉重新整理的庫
SVProgressHUD // 簡單易用的HUD複製程式碼
敲黑板
Moya的使用
Moya是基於Alamofire的網路請求庫,這裡我使用了Moya/Swift,它在Moya的基礎上新增了對RxSwift的介面支援。接下來我們來說下Moya的使用
一、建立一個列舉,用來存放請求型別,這裡我順便設定相應的路徑,等下統一取出來直接賦值即可
enum LXFNetworkTool {
enum LXFNetworkCategory: String {
case all = "all"
case android = "Android"
case ios = "iOS"
case welfare = "福利"
}
case data(type: LXFNetworkCategory, size:Int, index:Int)
}複製程式碼
二、為這個列舉寫一個擴充套件,並遵循塄 TargetType,這個協議的Moya這個庫規定的協議,可以按住Commond鍵+單擊左鍵進入相應的檔案進行檢視
extension LXFNetworkTool: TargetType {
/// baseURL 統一基本的URL
var baseURL: URL {
return URL(string: "http://gank.io/api/data/")!
}
/// path欄位會追加至baseURL後面
var path: String {
switch self {
case .data(let type, let size, let index):
return "(type.rawValue)/(size)/(index)"
}
}
/// HTTP的請求方式
var method: Moya.Method {
return .get
}
/// 請求引數(會在請求時進行編碼)
var parameters: [String: Any]? {
return nil
}
/// 引數編碼方式(這裡使用URL的預設方式)
var parameterEncoding: ParameterEncoding {
return URLEncoding.default
}
/// 這裡用於單元測試,不需要的就像我一樣隨便寫寫
var sampleData: Data {
return "LinXunFeng".data(using: .utf8)!
}
/// 將要被執行的任務(請求:request 下載:upload 上傳:download)
var task: Task {
return .request
}
/// 是否執行Alamofire驗證,預設值為false
var validate: Bool {
return false
}
}複製程式碼
三、定義一個全域性變數用於整個專案的網路請求
let lxfNetTool = RxMoyaProvider<LXFNetworkTool>()複製程式碼
至此,我們就可以使用這個全域性變數來請求資料了
RxDataSources
如果你想用傳統的方式也行,不過這就失去了使用RxSwift的意義。好吧,我們接下來說說如何優雅的來實現tableView的資料來源。其實RxDataSources官網上已經有很明確的使用說明,不過我還是總結一下整個過程吧。
概念點
RxDataSources是以section來做為資料結構來傳輸,這點很重要,可能很多同學會比較疑惑這句話吧,我在此舉個例子,在傳統的資料來源實現的方法中有一個numberOfSection,我們在很多情況下只需要一個section,所以這個方法可實現,也可以不實現,預設返回的就是1,這給我們帶來的一個迷惑點:【tableView是由row來組成的】,不知道在坐的各位中有沒有是這麼想的呢??有的話那從今天開始就要認清楚這一點,【tableView其實是由section組成的】,所以在使用RxDataSources的過程中,即使你的setion只有一個,那你也得返回一個section的陣列出去!!!
一、自定義Section
在我們自定義的Model中建立一個Section的結構體,並且建立一個擴充套件,遵循SectionModelType協議,實現相應的協議方法。約定俗成的寫法呢請參考如下方式
LXFModel.swift
struct LXFSection {
// items就是rows
var items: [Item]
// 你也可以這裡加你需要的東西,比如 headerView 的 title
}
extension LXFSection: SectionModelType {
// 重定義 Item 的型別為 LXFModel
typealias Item = LXFModel
// 實現協議中的方式
init(original: LXFSection, items: [LXFSection.Item]) {
self = original
self.items = items
}
}複製程式碼
二、在控制器下建立一個資料來源屬性
以下程式碼均在 LXFViewController.swift 檔案中
// 建立一個資料來源屬性,型別為自定義的Section型別
let dataSource = RxTableViewSectionedReloadDataSource<LXFSection>()複製程式碼
使用資料來源屬性繫結我們的cell
// 繫結cell
dataSource.configureCell = { ds, tv, ip, item in
// 這個地方使用了Reusable這個庫,在LXFViewCell中遵守了相應的協議
// 使其方便轉換cell為非可選型的相應的cell型別
let cell = tv.dequeueReusableCell(for: ip) as LXFViewCell
cell.picView.kf.setImage(with: URL(string: item.url))
cell.descLabel.text = "描述: (item.desc)"
cell.sourceLabel.text = "來源: (item.source)"
return cell
}複製程式碼
三、將sections序列繫結給我們的rows
output.sections.asDriver().drive(tableView.rx.items(dataSource:dataSource)).addDisposableTo(rx_disposeBag)複製程式碼
大功告成,接下來說說section序列的產生
ViewModel的規範
我們知道MVVM思想就是將原本在ViewController的檢視顯示邏輯、驗證邏輯、網路請求等程式碼存放於ViewModel中,讓我們手中的ViewController瘦身。這些邏輯由ViewModel負責,外界不需要關心,外界只需要結果,ViewModel也只需要將結果給到外界,基於此,我們定義了一個協議LXFViewModelType
一、建立一個LXFViewModelType.swift
LXFViewModelType.swift
// associatedtype 關鍵字 用來宣告一個型別的佔位符作為協議定義的一部分
protocol LXFViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}複製程式碼
二、viewModel遵守LXFViewModelType協議
- 我們可以為XFViewModelType的Input和Output定義別名,以示區分,如:你這個viewModel的用於請求首頁模組相關聯的,則可以命名為:HomeInput 和 HomeOutput
- 我們可以豐富我們的 Input 和 Output 。可以看到我為Output新增了一個序列,型別為我們自定義的LXFSection陣列,在Input裡面新增了一個請求型別(即要請求什麼資料,比如首頁的資料)
- 我們通過 transform 方法將input攜帶的資料進行處理,生成了一個Output
注意: 以下程式碼為了方便閱讀,進行了部分刪減
LXFViewModel.swift
extension LXFViewModel: LXFViewModelType {
// 存放著解析完成的模型陣列
let models = Variable<[LXFModel]>([])
// 為LXFViewModelType的Input和Output定義別名
typealias Input = LXFInput
typealias Output = LXFOutput
// 豐富我們的Input和Output
struct LXFInput {
// 網路請求型別
let category: LXFNetworkTool.LXFNetworkCategory
init(category: LXFNetworkTool.LXFNetworkCategory) {
self.category = category
}
}
struct LXFOutput {
// tableView的sections資料
let sections: Driver<[LXFSection]>
init(sections: Driver<[LXFSection]>) {
self.sections = sections
}
}
func transform(input: LXFViewModel.LXFInput) -> LXFViewModel.LXFOutput {
let sections = models.asObservable().map { (models) -> [LXFSection] in
// 當models的值被改變時會呼叫,這是Variable的特性
return [LXFSection(items: models)] // 返回section陣列
}.asDriver(onErrorJustReturn: [])
let output = LXFOutput(sections: sections)
// 接下來的程式碼是網路請求,請結合專案檢視,不然會不方便閱讀和理解
}
}複製程式碼
接著我們在ViewController中初始化我們的input,通過transform得到output,然後將我們output中的sections序列繫結tableView的items
LXFViewController.swift
// 初始化input
let vmInput = LXFViewModel.LXFInput(category: .welfare)
// 通過transform得到output
let vmOutput = viewModel.transform(input: vmInput)
vmOutput.sections.asDriver().drive(tableView.rx.items(dataSource: dataSource)).addDisposableTo(rx_disposeBag)複製程式碼
RxSwift中使用MJRefresh
一、定義一個列舉LXFRefreshStatus,用於標誌當前重新整理狀態
enum LXFRefreshStatus {
case none
case beingHeaderRefresh
case endHeaderRefresh
case beingFooterRefresh
case endFooterRefresh
case noMoreData
}複製程式碼
二、在LXFOutput新增一個refreshStatus序列,型別為LXFRefreshStatus
// 給外界訂閱,告訴外界的tableView當前的重新整理狀態
let refreshStatus = Variable<LXFRefreshStatus>(.none)複製程式碼
我們在進行網路請求並得到結果之後,修改refreshStatus的value為相應的LXFRefreshStatus項
三、外界訂閱output的refreshStatus
外界訂閱output的refreshStatus,並且根據接收到的值進行相應的操作
vmOutput.refreshStatus.asObservable().subscribe(onNext: {[weak self] status in
switch status {
case .beingHeaderRefresh:
self?.tableView.mj_header.beginRefreshing()
case .endHeaderRefresh:
self?.tableView.mj_header.endRefreshing()
case .beingFooterRefresh:
self?.tableView.mj_footer.beginRefreshing()
case .endFooterRefresh:
self?.tableView.mj_footer.endRefreshing()
case .noMoreData:
self?.tableView.mj_footer.endRefreshingWithNoMoreData()
default:
break
}
}).addDisposableTo(rx_disposeBag)複製程式碼
四、output提供一個requestCommond用於請求資料
PublishSubject 的特點:即可以作為Observable,也可以作為Observer,說白了就是可以傳送訊號,也可以訂閱訊號
// 外界通過該屬性告訴viewModel載入資料(傳入的值是為了標誌是否重新載入)
let requestCommond = PublishSubject<Bool>()複製程式碼
在transform中,我們對生成的output的requestCommond進行訂閱
output.requestCommond.subscribe(onNext: {[unowned self] isReloadData in
self.index = isReloadData ? 1 : self.index+1
lxfNetTool.request(.data(type: input.category, size: 10, index: self.index)).mapArray(LXFModel.self).subscribe({ [weak self] (event) in
switch event {
case let .next(modelArr):
self?.models.value = isReloadData ? modelArr : (self?.models.value ?? []) + modelArr
LXFProgressHUD.showSuccess("載入成功")
case let .error(error):
LXFProgressHUD.showError(error.localizedDescription)
case .completed:
output.refreshStatus.value = isReloadData ? .endHeaderRefresh : .endFooterRefresh
}
}).addDisposableTo(self.rx_disposeBag)
}).addDisposableTo(rx_disposeBag)複製程式碼
五、在ViewController中初始化重新整理控制元件
為tableView設定重新整理控制元件,並且在建立重新整理控制元件的回撥中使用output的requestCommond發射訊號
tableView.mj_header = MJRefreshNormalHeader(refreshingBlock: {
vmOutput.requestCommond.onNext(true)
})
tableView.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: {
vmOutput.requestCommond.onNext(false)
})複製程式碼
總結流程:
-
ViewController已經拿到output,當下拉載入資料的時候,使用output的requestCommond發射資訊,告訴viewModel我們要載入資料
-
viewModel請求資料,在處理完json轉模型或模型陣列後修改models,當models的值被修改的時候會發訊號給sections,sections在ViewController已經繫結到tableView的items了,所以此時tableView的資料會被更新。接著我們根據請求結果,修改output的refreshStatus屬性的值
-
當output的refreshStatus屬性的值改變後,會發射訊號,由於外界之前已經訂閱了output的refreshStatus,此時就會根據refreshStatus的新值來處理重新整理控制元件的狀態
好了,附上RxSwiftDemo。完結撒花