iOS - RxSwift 項目實戰記錄

ReactiveX

最近剛剛把接手的OC項目搞定,通過深思熟慮後,本人決定下個項目起就使用Swift(學了這麼久的Swift還沒真正用到實際項目裏。。。),而恰巧RxSwift已經出來有一些時間了,語法也基本上穩定,遂隻身前來試探試探這RxSwift,接着就作了個小Demo,有興趣的同窗能夠瞧一瞧~android

Exhibition

結構

.
├── 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的使用ios

1、建立一個枚舉,用來存放請求類型,這裏我順便設置相應的路徑,等下統一取出來直接賦值便可git

enum LXFNetworkTool {
    enum LXFNetworkCategory: String {
        case all     = "all"
        case android = "Android"
        case ios     = "iOS"
        case welfare = "福利"
    }
    case data(type: LXFNetworkCategory, size:Int, index:Int)
}
複製代碼

2、爲這個枚舉寫一個擴展,並遵循塄 TargetType,這個協議的Moya這個庫規定的協議,能夠按住Commond鍵+單擊左鍵進入相應的文件進行查看github

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
    }
}
複製代碼

3、定義一個全局變量用於整個項目的網絡請求json

let lxfNetTool = RxMoyaProvider<LXFNetworkTool>()
複製代碼

至此,咱們就可使用這個全局變量來請求數據了swift

RxDataSources

若是你想用傳統的方式也行,不過這就失去了使用RxSwift的意義。好吧,咱們接下來講說如何優雅的來實現tableView的數據源。其實RxDataSources官網上已經有很明確的使用說明,不過我仍是總結一下整個過程吧。api

概念點 RxDataSources是以section來作爲數據結構來傳輸,這點很重要,可能不少同窗會比較疑惑這句話吧,我在此舉個例子,在傳統的數據源實現的方法中有一個numberOfSection,咱們在不少狀況下只須要一個section,因此這個方法可實現,也能夠不實現,默認返回的就是1,這給咱們帶來的一個迷惑點:【tableView是由row來組成的】,不知道在坐的各位中有沒有是這麼想的呢??有的話那從今天開始就要認清楚這一點,【tableView實際上是由section組成的】,因此在使用RxDataSources的過程當中,即便你的setion只有一個,那你也得返回一個section的數組出去!!!數組

1、自定義Section 在咱們自定義的Model中建立一個Section的結構體,而且建立一個擴展,遵循SectionModelType協議,實現相應的協議方法。約定俗成的寫法呢請參考以下方式bash

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
    }
}
複製代碼

2、在控制器下建立一個數據源屬性微信

如下代碼均在 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
}
複製代碼

3、將sections序列綁定給咱們的rows

output.sections.asDriver().drive(tableView.rx.items(dataSource:dataSource)).addDisposableTo(rx_disposeBag)
複製代碼

大功告成,接下來講說section序列的產生

ViewModel的規範

咱們知道MVVM思想就是將本來在ViewController的視圖顯示邏輯、驗證邏輯、網絡請求等代碼存放於ViewModel中,讓咱們手中的ViewController瘦身。這些邏輯由ViewModel負責,外界不須要關心,外界只須要結果,ViewModel也只須要將結果給到外界,基於此,咱們定義了一個協議LXFViewModelType

1、建立一個LXFViewModelType.swift

LXFViewModelType.swift

// associatedtype 關鍵字 用來聲明一個類型的佔位符做爲協議定義的一部分
protocol LXFViewModelType {
    associatedtype Input
    associatedtype Output
    
    func transform(input: Input) -> Output
}
複製代碼

2、viewModel遵照LXFViewModelType協議

  1. 咱們能夠爲XFViewModelType的Input和Output定義別名,以示區分,如:你這個viewModel的用於請求首頁模塊相關聯的,則能夠命名爲:HomeInput 和 HomeOutput
  2. 咱們能夠豐富咱們的 Input 和 Output 。能夠看到我爲Output添加了一個序列,類型爲咱們自定義的LXFSection數組,在Input裏面添加了一個請求類型(即要請求什麼數據,好比首頁的數據)
  3. 咱們經過 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

1、定義一個枚舉LXFRefreshStatus,用於標誌當前刷新狀態

enum LXFRefreshStatus {
    case none
    case beingHeaderRefresh
    case endHeaderRefresh
    case beingFooterRefresh
    case endFooterRefresh
    case noMoreData
}
複製代碼

2、在LXFOutput添加一個refreshStatus序列,類型爲LXFRefreshStatus

// 給外界訂閱,告訴外界的tableView當前的刷新狀態
let refreshStatus = Variable<LXFRefreshStatus>(.none)
複製代碼

咱們在進行網絡請求並獲得結果以後,修改refreshStatus的value爲相應的LXFRefreshStatus項

3、外界訂閱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)
複製代碼

4、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)
複製代碼

5、在ViewController中初始化刷新控件

爲tableView設置刷新控件,而且在建立刷新控件的回調中使用output的requestCommond發射信號

tableView.mj_header = MJRefreshNormalHeader(refreshingBlock: { 
    vmOutput.requestCommond.onNext(true)
})
tableView.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: { 
    vmOutput.requestCommond.onNext(false)
})
複製代碼

總結流程:

  1. ViewController已經拿到output,當下拉加載數據的時候,使用output的requestCommond發射信息,告訴viewModel咱們要加載數據

  2. viewModel請求數據,在處理完json轉模型或模型數組後修改models,當models的值被修改的時候會發信號給sections,sections在ViewController已經綁定到tableView的items了,因此此時tableView的數據會被更新。接着咱們根據請求結果,修改output的refreshStatus屬性的值

  3. 當output的refreshStatus屬性的值改變後,會發射信號,因爲外界以前已經訂閱了output的refreshStatus,此時就會根據refreshStatus的新值來處理刷新控件的狀態

好了,附上RxSwiftDemo。完結撒花

微信公衆號
相關文章
相關標籤/搜索