【漫談】從項目實踐走向RxSwift響應(yīng)式函數(shù)編程

RxSwift.png

(一)萬年不變的開端

去年大三還在學(xué)校的時候就聽說過ReactiveCocoa這一Github開源的響應(yīng)式重量級框架,可是對于當(dāng)時還只埋頭狂寫OOP的我來說,大概只能用下面的話來形容自己吧。

你對力量一無所知。

后來為了學(xué)習(xí)ReactiveCocoa,看了幾乎所有中文的資料以及博客,才感覺自己稍稍入門。相對于OOP,的確FRP的學(xué)習(xí)成本高了很多,但是也絕對沒有有些人說的那么夸張,可能就有那么一些人,會了一些國外玩了很久的技術(shù),然后沾沾自喜地指著其他人,“看!我多么厲害,這么難的技術(shù)我都學(xué)會了!”,似乎只有這樣才能體現(xiàn)他們卑微的優(yōu)越感。

聞道有先后。

我一直相信這句話。

在這里不去討論FRP多么的優(yōu)越,每一種技術(shù)棧都有自己的優(yōu)劣勢,學(xué)習(xí)FRP對我來說更多的是開拓自己的視野,擴展技能棧。在早期的創(chuàng)業(yè)項目中,過早的引入復(fù)雜的架構(gòu),“先進”的框架,我想更多的會是得不償失吧。

(二)MVVM Pattern

MVVM.png

其實移動端的技術(shù)一直慢于Web端,Web前端事件驅(qū)動,數(shù)據(jù)驅(qū)動已經(jīng)晚了很多年了,而對于iOS平臺來說RxSwift,ReSwift都是比較新的框架,但是他們都無一例外 Inspire By XXX。

從 MVC -> MVVM,無非是每一個模塊都更加的純粹,給原來充斥著各種各樣業(yè)務(wù)代碼的控制器“瘦身”,ViewModel所提供給View的應(yīng)該是可以直接展示或者可以直接根據(jù)狀態(tài)來綁定的接口。

一般來說,經(jīng)典的業(yè)務(wù)情況將是這樣的:

經(jīng)典業(yè)務(wù)邏輯.jpeg

首先我們通過本地的網(wǎng)絡(luò)層獲取到服務(wù)器通信給我們的MetaData,一般來說是JSON或者XML,然后通過客戶端的解析(transform),構(gòu)造出對應(yīng)的Model,但是Model對象的值并不能直接展示給用戶,所以我們需要對它做加工處理(Process)。例如,UserModel中性別通過Int類型的1,2來區(qū)分,于是我們通過本地的邏輯代碼,將其轉(zhuǎn)化成字符串類型的男,女,而這些最后的字符串類型的數(shù)據(jù)就是ViewModel所暴露出來的數(shù)據(jù),View層就可以通過ViewModel所暴露出來的數(shù)據(jù)綁定現(xiàn)實了。

(三)MVVM 實踐 Demo:Gank.io客戶端

1.初窺

首先感謝代碼家傾情提供的api,當(dāng)你想通過實際項目來提升自己的時候,別猶豫,先用這個api來擼一個gank客戶端吧!

這個gank客戶端的首頁大概是這樣子的。

Simulator Screen Shot 2017年2月24日 上午2.24.10.png

2.關(guān)于ViewModel

一個好的ViewModel是怎么樣的 ?

devxoulRxTodo的Readme中所寫的關(guān)于ViewModel的哲學(xué),我深以為然:

  • View 不應(yīng)該存在邏輯控制流的邏輯,View 不應(yīng)該對數(shù)據(jù)造成操作,View 只能綁定數(shù)據(jù),控制顯示。

    Bad

    viewModel.titleLabelText
      .map { $0 + "!" }  // Bad: View 不應(yīng)該有修改數(shù)據(jù)的操作
      .bindTo(self.titleLabel)
    

    Good

    viewModel.titleLabelText
      .bindTo(self.titleLabel.rx.text)
    
  • View 并不知道 ViewModel具體做了什么,View 只能通過ViewModel知道需要View做什么.

    Bad

    viewModel.login() // Bad: View不應(yīng)該知道ViewModel的具體動作
    

    Good

    self.loginButton.rx.tap
      .bindTo(viewModel.loginButtonDidTap) 
    
    self.usernameInput.rx.controlEvent(.editingDidEndOnExit)
      .bindTo(viewModel.usernameInputDidReturn) // "Hey I tapped the return on username input"
    
  • Model 應(yīng)當(dāng)被ViewModel所隱藏,ViewModel只暴露出View渲染所需要的最少信息。

    Bad

    struct ProductViewModel {
      let product: Driver<Product> // Bad: Model不應(yīng)該暴露給View,也不需要暴露給View.
    }
    

    Good

    struct ProductViewModel {
      let productName: Driver<String>
      let formattedPrice: Driver<String>
      let formattedOriginalPrice: Driver<String>
      let isOriginalPriceHidden: Driver<Bool>
    }
    

個人在實踐的時候也是完全遵從這幾個原則的,最主要的好處是,這樣的代碼結(jié)構(gòu)使得App層次分明,實踐在RxSwift上面大概是這樣的:

    //告訴View當(dāng)前的分類(iOS,android,后端等等...)
    let category = Variable<Int>(0) 
    // RxDataSources所需要的數(shù)據(jù)模型,用于優(yōu)雅的綁定TableView
    let section: Driver<[HomeSection]> 
    // 首頁的刷新命令,入?yún)⑹钱?dāng)前的分類
    let refreshCommand = PublishSubject<Int>() 
    // 刷新當(dāng)前的頁面,和下拉操作一起綁定
    let refreshTrigger = PublishSubject<Void>() 
    // RxDataSource的核心類
    let dataSource = RxTableViewSectionedReloadDataSource<HomeSection>() 
    // 被隱藏起來的Model
    fileprivate let bricks = Variable<[Brick]>([])

在ViewModel的構(gòu)造方法中,初始化相關(guān)的變量

    override init() {
        
        section = bricks.asObservable().map({ (bricks) -> [HomeSection] in
            return [HomeSection(items: bricks)]
        })
        .asDriver(onErrorJustReturn: [])
    
        super.init()
        
        refreshCommand
            .flatMapLatest { gankApi.request(.data(type: GankAPI.GankCategory.mapCategory(with: $0), size: 20, index: 0)) }
            .subscribe({ [weak self] (event) in
                self?.refreshTrigger.onNext()
                switch event {
                case let .next(response):
                    do {
                        let data = try response.mapArray(Brick.self)
                        self?.bricks.value = data
                    }catch {
                        self?.bricks.value = []
                    }
                    break
                case let .error(error):
                    self?.refreshTrigger.onError(error);
                    break
                default:
                    break
                }
            })
            .addDisposableTo(rx_disposeBag)

    }

到這里,我們Gank客戶端的ViewModel就算是構(gòu)建完成了,遵守前面說到的設(shè)計哲學(xué),ViewModel還是很好構(gòu)建的。

Binding

最后的一步就是將ViewModel中暴露出來的綁定給View(控制器或者是View),在RxSwift的世界中,綁定的操作是通過鏈式的方法調(diào)用來玩成的。值得一說的是,在“競爭對手”ReactiveSwift中,大多數(shù)的綁定都可以通過自定義的操作符<~來實現(xiàn),不得不說,貌似在這方面,ReactiveSwift更加全面的利用了Swift的語言特性,看起來也更加裝逼了呢?;氐竭@個Gank項目,Controller層面大概是這樣的:



    do /** Rx Config */ {
    
        // Input
        
        tableView.refreshControl?.rx.controlEvent(.allEvents)
            .flatMap({ self.homeVM.category.asObservable() })
            .bindTo(homeVM.refreshCommand)
            .addDisposableTo(rx_disposeBag)
        
        NotificationCenter.default.rx.notification(Notification.Name.category)
            .map({ (notification) -> Int in
                let indexPath = (notification.object as? IndexPath) ?? IndexPath(item: 0, section: 0)
                return indexPath.row
            })
            .bindTo(homeVM.category)
            .addDisposableTo(rx_disposeBag)
        

        NotificationCenter.default.rx.notification(Notification.Name.category)
            .map({ (notification) -> Int in
                let indexPath = (notification.object as? IndexPath) ?? IndexPath(item: 0, section: 0)
                return indexPath.row
            })
            .observeOn(MainScheduler.asyncInstance)
            .do(onNext: { (idx) in
                SideMenuManager.menuLeftNavigationController?.dismiss(animated: true, completion: {
                    DispatchQueue.main.async(execute: { 
                        self.tableView.refreshControl?.beginRefreshing()
                    })
                })
            }, onError: nil, onCompleted: nil, onSubscribe:nil,onDispose: nil)
            .bindTo(homeVM.refreshCommand)
            .addDisposableTo(rx_disposeBag)
        
        // Output
        
        homeVM.section
            .drive(tableView.rx.items(dataSource: homeVM.dataSource))
            .addDisposableTo(rx_disposeBag)
        
        tableView.rx.setDelegate(self)
            .addDisposableTo(rx_disposeBag)
        
        homeVM.refreshTrigger
            .observeOn(MainScheduler.instance)
            .subscribe { [unowned self] (event) in
                self.tableView.refreshControl?.endRefreshing()
                switch event {
                case .error(_):
                    NoticeBar(title: "Network Disconnect!", defaultType: .error).show(duration: 2.0, completed: nil)
                    break
                case .next(_):
                    self.tableView.reloadData()
                    break
                default:
                    break
                }
            }
            .addDisposableTo(rx_disposeBag)
        
        // Configure
        
        homeVM.dataSource.configureCell = { dataSource, tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(for: indexPath, cellType: HomeTableViewCell.self)
            cell.gankTitle?.text = item.desc
            cell.gankAuthor.text = item.who
            cell.gankTime.text = item.publishedAt.toString(format: "YYYY/MM/DD")
            return cell
        }
    }


2.強大的RxSwift社區(qū)開源組件

在Github上的RxSwift Community項目列表中,我們可以看到大量的一系列RxSwift拓展,并且任然還有大量的開發(fā)者為其添磚加瓦,在這里我們可以看到一個朝氣蓬勃的社區(qū)。

在這里列舉一些比較常用的擴展組件,如果想了解更多的組件,可以點擊上文的鏈接關(guān)注他們。

(1). Action

使用過ReactiveCocoa的人一定不會對RACCommand陌生,RACCommand是一個將用戶的操作封裝起來的組件。它所發(fā)射的信號是一個二階信號,即發(fā)射的信號的值是一個信號。在RxSwift的世界里,Action就充當(dāng)了類似的功能。

(2). NSObject-Rx

NSObject-Rx,在我看來這個擴展對于RxSwift開發(fā)者來說是最簡單同時也是必備的,為了合理的內(nèi)存管理,我們總要寫上這樣的代碼:

class MyObject: Whatever {
    let disposeBag = DisposeBag()

    ...
}

然后重復(fù)...重復(fù)...再重復(fù)的這樣寫,本來使用RxSwift如此優(yōu)雅的FRP,著實被這個DisposeBag給煞了風(fēng)景。但是當(dāng)引用了NSObject-Rx之后,通過Swift Extension的方式,雖然還是需要顯示的加上DisposeBag,但是已經(jīng)相比之前優(yōu)雅太多。


thing
  .bindTo(otherThing)
  .addDisposableTo(rx_disposeBag)

整個世界都安靜了... ---------《大話西游》

(3).RxDataSources

講道理,RxDataSources也是我們在開發(fā) RxApp 時候的標配。如果你用了RxSwift,然后還在那里傻傻的寫著TableView的Delegate,那我覺得不如回家養(yǎng)豬,還寫啥Swift ?通過RxDataSources我們可以很完美的將VM和View綁定起來,做到優(yōu)雅的分層處理和數(shù)據(jù)驅(qū)動。我們可以來看看我在Gank客戶端中RxDataSources的示例用法:


/// 首先需要構(gòu)建一個Section模型,遵守SectionModelType協(xié)議
struct HomeSection {
    
    var items: [Item]
}

extension HomeSection: SectionModelType {
    
    typealias Item = Brick
    
    init(original: HomeSection, items: [HomeSection.Item]) {
        self = original
        self.items = items
    }
}

然后我們需要一個DataSource的示例驅(qū)動對象

    /// 使用之前我們構(gòu)造的HomeSection
    let dataSource = RxTableViewSectionedReloadDataSource<HomeSection>()

最后我們只需要將ViewModel中的數(shù)據(jù)模型通過RxDataSource與TableView或者CollectionView綁定即可。


    // Binding with Section Model and UITableView    
    homeVM.section
        .drive(tableView.rx.items(dataSource: homeVM.dataSource))
        .addDisposableTo(rx_disposeBag)

完美,忘掉那些一大推的系統(tǒng)原生Delegate吧...

結(jié)語

我們可以看到,RxSwift有一個非常活躍和富有創(chuàng)造性的社區(qū),為優(yōu)雅的使用Rx提供了一系列的組件,雖然RxSwift上手需要一點點的功夫,但是這些一系列的組件又為我們降低了門檻。從Star數(shù)來看,似乎使用的人還不是很多,筆者在這里的建議就是:** 趕快上車RxSwift **。

RxSwift + MVVM + Services + Routing + Moya ,這是筆者比較喜歡的架構(gòu)方式,本片文章更多的是介紹性質(zhì),很多東西都是一略而過,并沒有什么太多的技術(shù)含量,只是希望更多的人了解未來,面向未來,向未來前進。

這個是本文的Demo地址,僅供參考Gank客戶端。

你看那個人,好像一條狗哎...

(EOF)參考文章

  1. 被誤解的MVC和被神化的MVVM
  2. Coordinator 與 RxSwift 共同構(gòu)建的 MVVM 架構(gòu)
  3. iOS 架構(gòu)模式 - 簡述 MVC, MVP, MVVM 和 VIPER
  4. MVVM With ReactiveCocoa
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容