iOS - RxSwift 專案實戰記錄

LinXunFeng發表於2017-09-27

ReactiveX

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

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的使用

一、建立一個列舉,用來存放請求型別,這裡我順便設定相應的路徑,等下統一取出來直接賦值即可

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協議

  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

一、定義一個列舉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)
})
複製程式碼

總結流程:

  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。完結撒花

微信公眾號

相關文章