很好懂的Swift MVVM in Rx

并不是說(shuō)Rx就是MVVM,只是用了Rx,才能更有MVVM的趕腳,畢竟iOS原生的MVC框架沒(méi)那么好改變~

所以你以為接下來(lái)要寫什么是MVVM了嘛?自己去百度吧

所以你以為接下來(lái)我要寫什么是Rx了嘛?自己去Github吧

首先有些概念

1. MVVM有哪些文件

簡(jiǎn)單來(lái)說(shuō)就是我們有ControllerViewModel兩個(gè)文件,去做一坨東西,Controller操作UI,ViewModel操作業(yè)務(wù),(嗯,這個(gè)是整篇里說(shuō)的最官方的了)

2. ControllerViewModel怎么交互?

當(dāng)然如果我說(shuō)Controller里寫viewModel.doSth()這樣,你肯定就不會(huì)看下去了;

所以我們引入一個(gè)【節(jié)點(diǎn)】的概念,節(jié)點(diǎn)有兩三種,

一種是凸凸的節(jié)點(diǎn),代表著他會(huì)向外發(fā)射一個(gè)能量,至于這能量是個(gè)啥,你先別管~對(duì)于凸凸,通常情況下你只需要給他來(lái)一發(fā)就完了,也可能你需要包裝一下里面的能量,比如把貓糧裝到盒子包起來(lái)然后再發(fā)出去

一種是凹凹的節(jié)點(diǎn),代表著他需要外面給他來(lái)一發(fā)能量,這個(gè)要做的事情比較多,你收到能量后需要拿著他去做接下來(lái)的事,這是你對(duì)應(yīng)的凸凸需要你完成的,或者說(shuō)你需要把快遞拆了,然后把貓糧拆了,然后去喂貓,然后觀察貓是不是正常,是不是喜歡吃,然后如果不正常還需要退貨去罵人~

一種是凹凸的節(jié)點(diǎn),這就很牛皮了他既能來(lái)一發(fā)又能收一發(fā),所以上面提到的他都要干

而至于能量/快遞盒子是神馬?方法傳遞時(shí)候不是需要帶參數(shù)么,你把參數(shù)當(dāng)成能量也闊以~

有點(diǎn)抽象?看下面

class ViewController: UIViewController {
    let viewModel = ViewModel()
    func buttonTapped()
    {
        viewModel.dosth(一發(fā)能量)
    }
}

這是普通版本的,而在節(jié)點(diǎn)之下,Controller中會(huì)有個(gè)凸凸的節(jié)點(diǎn),而ViewModel就會(huì)有個(gè)凹凹的節(jié)點(diǎn),然后用厲害的手段把他們結(jié)合起來(lái)。

class ViewController: UIViewController
{
    let signalOutput = 凸凸<能量>()
    
    override func viewDidLoad() {
        
        // 把凸凸和凹凹結(jié)合到一起
        // 別想那些羞射的事情,不是那樣的!
        putTogether(signalOutput , signalInput)
    }
}

class ViewModel
{
    let signalInput = 凹凹<能量>()
}

所以說(shuō)白了就是:
我們把所有方法調(diào)用變成了屬性,所以我們不再存在方法調(diào)用了,方法會(huì)濃縮到屬性里去,當(dāng)然你如果把【凹凹】理解成封裝了個(gè)closure,那我覺(jué)得你已經(jīng)領(lǐng)悟了很多了~,所以下面你應(yīng)該有這么個(gè)概念,

Button點(diǎn)擊之類的事情,我就需要有個(gè)【凸凸】,同時(shí)我需要在ViewModel中找個(gè)【凹凹】,把他們捏一起讓他們肚子里的能量傳遞起來(lái),

不過(guò)Button點(diǎn)下去,你啥都沒(méi),所以只是一個(gè)空的能量在那傳遞,傳遞還是會(huì)傳,只是沒(méi)什么內(nèi)容,就像你收到一個(gè)空的快遞盒子一樣~

還是不理解的話試一下最后一個(gè)場(chǎng)景,把凸凸想成送快遞的,把凹凹想成收快遞的,而里面的能量就是快遞盒子,再想一下,如果還想不通,那就回過(guò)去再看看吧~






所以我們從三個(gè)簡(jiǎn)單的實(shí)際場(chǎng)景看下,在我當(dāng)前的思維模式下,是怎么把簡(jiǎn)單問(wèn)題變得灰常復(fù)雜的,這其實(shí)就是MVVM干的事情~

場(chǎng)景一:支付場(chǎng)景下的簡(jiǎn)單判斷

現(xiàn)在有個(gè)按鈕,點(diǎn)擊后觸發(fā)一個(gè)請(qǐng)求告訴我們用戶有木有設(shè)置過(guò)支付密碼/TouchId/甚至FaceId,怎么高端你就怎么想;

如果有設(shè)置過(guò),直接讓用戶輸入密碼/摸手機(jī)/秀臉,操作完成后帶著密碼提交請(qǐng)求,然后完成支付。流程結(jié)束

如果木有設(shè)置過(guò),那就跳轉(zhuǎn)到設(shè)置頁(yè)面結(jié)束。流程結(jié)束

這應(yīng)該描述起來(lái)很簡(jiǎn)單,但是如果按照普通的寫法,不是Controller會(huì)變的很肥,就是流程間的交互Closure會(huì)很多

所以該怎么把這個(gè)看著簡(jiǎn)單的東西解析成看著灰常復(fù)雜的場(chǎng)景呢~

1. 首先我們要知道Controller要做些啥

  • 按鈕點(diǎn)擊
  • 彈個(gè)密碼框讓用戶輸
  • 跳轉(zhuǎn)去設(shè)置密碼

上面是在MVVM中,Controller需要做的事,只有這些,沒(méi)有別的了

所以這三個(gè)事情一定會(huì)對(duì)應(yīng)三個(gè)節(jié)點(diǎn)了,那哪些是凸凸哪些是凹凹呢?
對(duì)于Controller來(lái)說(shuō)很簡(jiǎn)單的概念就是,所有從View觸發(fā)/或者說(shuō)從界面觸發(fā)的東西都是凸凸,比如按鈕點(diǎn)擊,比如列表cell點(diǎn)擊等。

所以上面這三個(gè)

  • 按鈕點(diǎn)擊 【我是凸凸】
  • 輸入完成密碼【我是凸凸】
  • 彈個(gè)密碼框讓用戶輸 【我是凹凹】
  • 跳轉(zhuǎn)去設(shè)置密碼 【我是凹凹】

可能我們會(huì)問(wèn),為什么下面2個(gè)是凹凹,因?yàn)樗灰彩怯|發(fā)UI嘛?但是如果這樣問(wèn),誰(shuí)讓你的按鈕點(diǎn)擊了,誰(shuí)讓你彈個(gè)密碼框了,誰(shuí)讓你跳轉(zhuǎn)了,你就知道了,除了第一第二個(gè)是用戶點(diǎn)的,其他兩個(gè)其實(shí)都是別的代碼讓你觸發(fā)的,所以在這兩個(gè)文件中,只有可能是ViewModelController去做了這兩件事


2. ViewModel要做些啥

  • 一個(gè)查詢是不是有支付密碼的請(qǐng)求
  • 處理是不是有支付密碼然后分發(fā)
  • 告訴Controller彈出密碼框
  • 告訴Controller去設(shè)置密碼
  • 接受個(gè)密碼
  • 一個(gè)提交支付密碼的請(qǐng)求
  • 告訴Controller支付成功了

ViewModel要做的事就多了,但是我們簡(jiǎn)單解析后就這些了,還是和上面一樣,我們把凸凸凹凹分析一下,ViewModel如果理解了,那就真的明白這個(gè)概念了,其實(shí)增的很簡(jiǎn)單。

  • 一個(gè)查詢是不是有支付密碼的請(qǐng)求 【凹凹】
  • 處理是不是有支付密碼然后分發(fā) 【凹凹】
  • 告訴Controller彈出密碼框 【凸凸】
  • 告訴Controller去設(shè)置密碼 【凸凸】
  • 接受個(gè)密碼 【凹凹】
  • 一個(gè)提交支付密碼的請(qǐng)求 【凹凹】
  • 告訴Controller支付成功了 【凸凸】

其實(shí)凸凸很好理解,都告訴Controller做事了,還不是凸凸也真是服。而請(qǐng)求發(fā)起者在這個(gè)場(chǎng)景下都是來(lái)自于Controller,而第二個(gè)處理是否有密碼的節(jié)點(diǎn)其實(shí)只是個(gè)過(guò)度節(jié)點(diǎn),而同樣的過(guò)度還會(huì)在請(qǐng)求中出現(xiàn),當(dāng)然這之后再說(shuō);

所以查詢支付密碼后,請(qǐng)求會(huì)有個(gè)能量傳遞給【處理是否有支付密碼的分發(fā)】,這就屬于內(nèi)部凸凸凹凹了,用的順的話這就會(huì)很習(xí)慣。

在有了這些概念后來(lái)看張整合的圖:


image

這里有幾個(gè)是內(nèi)部向下箭頭,其實(shí)凸凸也是可以接受內(nèi)部來(lái)的能量的,換句話說(shuō)他對(duì)外可能是凸凸,對(duì)內(nèi),那就未必咯~


3. Controller的凹凹

首先你要知道凸凸和凹凹是個(gè)啥~,我們就非常生硬的引入了RxSwift的概念,反正只是個(gè)類名而已,不會(huì)影響大家任何思路:
我們把單個(gè)節(jié)點(diǎn)定義成:

let signalOutput = PublishRelay<Void>()

其中PublishRelay就是凸凸和凹凹的類型,其實(shí)他們是同一個(gè)類的,只是從功能上區(qū)分了凸和凹的概念,在代碼上沒(méi)什么差別。
另外PublishRelayPublishSubject的簡(jiǎn)單封裝,作用一毛一樣,區(qū)別就是前者在RxCocoa中,而后者在RxSwift中,所以單純用這個(gè),我們就可以來(lái)看下Controller會(huì)變成什么樣:

class ViewController: UIViewController {
    let button = UIButton()
    // 按鈕點(diǎn)擊
    let buttonTapOutput = PublishRelay<Void>()
    
    // 跳轉(zhuǎn)去設(shè)置支付密碼
    let gotoSetpswInput = PublishRelay<Void>()
    
    // 彈密碼輸入框
    let showSetpswInput = PublishRelay<Void>()
    
    // 輸入密碼完成
    let finishedSettingpswOutput = PublishRelay<String>()
    
    // 流程完成
    let allFinishedInput = PublishRelay<String>()
    
    override func viewDidLoad() {
        
        button.rx.tap
            .bind(to: buttonTapOutput)
            .disposed(by: rx.disposeBag)
        
        gotoSetpswInput.subscribe(onNext: { (_) in
            // 此處添加navigate跳轉(zhuǎn)邏輯
            print("跳轉(zhuǎn)啦啦啦")
            
        }).disposed(by: rx.disposeBag)
        
        showSetpswInput.subscribe(onNext: { (_) in
            // 此處添加彈出密碼設(shè)置
            print("設(shè)置密碼啦啦啦")
            
        }).disposed(by: rx.disposeBag)
        
        allFinishedInput.subscribe(onNext: { (_) in
            print("all done")
            
        }).disposed(by: rx.disposeBag)
    }
}

如果無(wú)法理解,你可以理解成bind就是把凸凸和凹凹結(jié)合的方法,當(dāng)然也可以連接兩個(gè)凸凸,這樣相當(dāng)于兩個(gè)快遞員之間的傳遞,而subscribeNext 的用法在這就是給凹凹添加一系列處理事件,他在獲取到能量后需要做一堆事,比如我們可以在設(shè)置密碼處彈框,在跳轉(zhuǎn)處寫navigationController.push()...

再詳細(xì)點(diǎn)描述,其實(shí)bindsubscribe的用法是一樣的,只是bind給另一個(gè)節(jié)點(diǎn),相當(dāng)于多了個(gè)快遞傳送,但是最后的closure肯定需要的,只是你什么時(shí)候?qū)懚?,?code>subscribe就是你懶的找快遞了直接開(kāi)箱驗(yàn)貨了而已

上面的代碼中其實(shí)只處理了Controller的input,在那張圖上就是箭頭指向Controller的部分,因?yàn)檫@就是Controller要做的事,所以在MVVM崇尚VC/VM分離的情況下,我們可以毫無(wú)干涉的先把Controller要做的部分完善掉,無(wú)需考慮任何別的業(yè)務(wù)邏輯,這也是隔離的一個(gè)好處


4. ViewModel的凹凹

從上面這點(diǎn)我們可以看出,當(dāng)我們劃分清楚了凹凹凸凸,我們的開(kāi)發(fā)順序會(huì)變的順暢,處理完Controller的凹凹之后,我們接下來(lái)需要處理ViewModel的凹凹,因?yàn)榘及级际钱?dāng)前類肚子中的邏輯,所以會(huì)很好做

class ViewModel {
    let disposeBag = DisposeBag()
    
    // 查詢是否有支付密碼,帶請(qǐng)求
    let checkPswInput = PublishRelay<Void>()
    
    // 處理查詢結(jié)果
    let checkeNeedPswDispatch = PublishRelay<Bool>()
    
    // 去設(shè)置密碼
    let gotoSetpswOutput = PublishRelay<Void>()
    
    // 彈出框輸入密碼
    let showSetpswOutput = PublishRelay<Void>()
    
    // 密碼輸入完成
    let setPswFinishInput = PublishRelay<String>()
    
    // all done
    let finishedAllOuput = PublishRelay<Void>()
    
    init() {
        
        // flatMap后的內(nèi)容其實(shí)應(yīng)該用一個(gè)請(qǐng)求代替,這里簡(jiǎn)略一下直接把返回值標(biāo)出來(lái)了
        // 一般Rx的請(qǐng)求返回的結(jié)果就是Observable<T>的
        checkPswInput
            .flatMap { Observable.just(true) }
            .bind(to: checkeNeedPswDispatch)
            .disposed(by: disposeBag)
        
        checkeNeedPswDispatch.subscribe(onNext: { (hasPsw) in
            if hasPsw {
                self.showSetpswOutput.accept(())
            }
            else {
                self.gotoSetpswOutput.accept(())
            }
        }).disposed(by: disposeBag)
        
        setPswFinishInput
            .flatMap { _ in Observable.just(true) }
            .map { _ in () }.bind(to: finishedAllOuput)
            .disposed(by: disposeBag)
    }
}

因?yàn)橹攸c(diǎn)不在請(qǐng)求,所以我們簡(jiǎn)單忽略請(qǐng)求部分,把最終結(jié)果貼了上來(lái),flatMapMap簡(jiǎn)單來(lái)說(shuō)就是快遞拿到你的包裹給你加了個(gè)盒子或者拆了個(gè)盒子換了個(gè)信封之類的,雖然這很不道德,但是在Rx中這再正常不過(guò),雖然這有點(diǎn)響應(yīng)式的概念,如果想了解的話可以點(diǎn)一下看看,不過(guò)如果看不懂:

所謂響應(yīng)式,就是你在網(wǎng)上買了袋貓糧,賣家把貨給一個(gè)快遞,途中他可能給你裝盒子,拆盒子,拿出來(lái)嘗一口,給你換一袋便宜的,交給另一個(gè)快遞小哥,搭個(gè)飛機(jī)乘個(gè)坦克,然后給你重新買袋正品,最后送到你的手上的還是一袋東西,那至于他是不是你想要的貓糧,就要看中間的過(guò)程了,但是這件事就是有始有終的一整個(gè)工作流,途中凹凹凸凸凸凸凹凹啥的一大堆,最終就是一袋東西在那傳,送到你手上。

好了不扯遠(yuǎn),在上面的代碼之后,其實(shí)我們已經(jīng)可以為Controller寫好凸凸來(lái)綁定ViewModel的凹凹,反之亦然

5. ControllerViewModel的凸凸凹凹綁定

class ViewController: UIViewController
{
    let button = UIButton()
    
    let viewModel = ViewModel()
    // 按鈕點(diǎn)擊
    let buttonTapOutput = PublishRelay<Void>()
    
    // 跳轉(zhuǎn)去設(shè)置支付密碼
    let gotoSetpswInput = PublishRelay<Void>()
    
    // 彈密碼輸入框
    let showSetpswInput = PublishRelay<Void>()
    
    // 輸入密碼完成
    let finishedSettingpswOutput = PublishRelay<String>()
    
    // 流程完成
    let allFinishedInput = PublishRelay<Void>()
    
    override func viewDidLoad() {
        
        button.rx.tap
            .bind(to: buttonTapOutput)
            .disposed(by: rx.disposeBag)
        
        gotoSetpswInput.subscribe(onNext: { (_) in
            // 此處添加navigate跳轉(zhuǎn)邏輯
            print("跳轉(zhuǎn)啦啦啦")
            
        }).disposed(by: rx.disposeBag)
        
        showSetpswInput.subscribe(onNext: { (_) in
            // 此處添加彈出密碼設(shè)置
            print("設(shè)置密碼啦啦啦")
            
        }).disposed(by: rx.disposeBag)
        
        allFinishedInput.subscribe(onNext: { (_) in
            print("all done")
            
        }).disposed(by: rx.disposeBag)
        
        
        // Controller <=> ViewModel 綁定
        buttonTapOutput
            .bind(to: viewModel.checkPswInput)
            .disposed(by: rx.disposeBag)
        
        finishedSettingpswOutput
            .bind(to: viewModel.setPswFinishInput)
            .disposed(by: rx.disposeBag)
        
        viewModel.gotoSetpswOutput.bind(to: gotoSetpswInput).disposed(by: rx.disposeBag)
        viewModel.showSetpswOutput.bind(to: showSetpswInput).disposed(by: rx.disposeBag)
        viewModel.finishedAllOuput.bind(to: allFinishedInput).disposed(by: rx.disposeBag)
    }
}

我們?cè)?code>ViewDidLoad最后添加了點(diǎn)綁定方法,這樣,這個(gè)場(chǎng)景中所有線的凸凸凹凹就全部被結(jié)合到一起了

就這樣,一個(gè)看似復(fù)雜的業(yè)務(wù)流程被解析成了更復(fù)雜的MVVM的Rx形態(tài),不過(guò)是不是在開(kāi)發(fā)思路上順暢了不少呢?大概這就是為什么我們會(huì)用這套東西的原因吧


場(chǎng)景二:添加刪除列表某一項(xiàng)

為什么拿這么簡(jiǎn)單的場(chǎng)景舉例,因?yàn)檫@能很好的體現(xiàn)這套思維的另一個(gè)特征,如果說(shuō)場(chǎng)景一中是帶著你繞圈子,作用是強(qiáng)化一個(gè)個(gè)節(jié)點(diǎn)的概念;那這個(gè)場(chǎng)景就是強(qiáng)調(diào)【數(shù)據(jù)驅(qū)動(dòng)】的概念了

簡(jiǎn)單來(lái)說(shuō)就是你一定希望,我的Array中少一項(xiàng),那以他為數(shù)據(jù)源的tableView就會(huì)自動(dòng)少一行。其實(shí)這個(gè)場(chǎng)景就是幫助我們達(dá)到這個(gè)目的的

所以我們先請(qǐng)我們的數(shù)據(jù)源來(lái)亮相一下:

var sectionedData: BehaviorRelay<[MGItem>]>!

有人會(huì)說(shuō)你作弊,BehaviorRelay又是啥,其實(shí)他就是個(gè)有貨的凸凸凹凹,在前面的PublishRelay中那是個(gè)快遞的角色的話,記住PublishRelay只會(huì)傳遞能量,他不會(huì)留存你的能量,即快遞拿到你的貨物后,給到下一個(gè)之后快遞是不會(huì)保留的你貓糧的,畢竟他不養(yǎng)貓留著也沒(méi)啥用。

但是在這個(gè)場(chǎng)景中,我們需要一個(gè)長(zhǎng)期持有能量的節(jié)點(diǎn)來(lái)充當(dāng)數(shù)據(jù)源,畢竟你的數(shù)據(jù)源如果只是個(gè)快遞小哥,那他可能啥都沒(méi)~,

所以我們用一個(gè)很夸張的例子來(lái)理解BehaviorRelay,把他理解成你善良的母親大人就行了,有一天你把貓糧給你的母親大人,讓她幫你傳達(dá)給你的伙伴,于是這袋貓糧就永遠(yuǎn)留在了你母親大人的心里,雖然最后她會(huì)交給你的伙伴,但是如果這時(shí)候你說(shuō),我還要一袋,我還要兩袋,你母親大人總能幫你變出來(lái),因?yàn)樗呀?jīng)記住了貓糧長(zhǎng)什么樣了,所以你就可以理解,你無(wú)限的索取,你母親大人會(huì)幫你變出一集裝箱的貓糧,而且他們是同一個(gè)樣子的。

專業(yè)點(diǎn)就是,你母親大人不停給你貓糧指針,指向同一個(gè)地址,地址就在她腦子里,所以哪天你說(shuō)我要狗糧的時(shí)候,你母親大人的腦子里就會(huì)立馬變成狗糧,巴不得她把所有貓糧全變成狗糧,雖然現(xiàn)實(shí)中她做不到。

有點(diǎn)啰嗦,所以BehaviorRelay就是可以永久持有一個(gè)能量,并且你做任何改動(dòng),能量都會(huì)被永久變化的東西,用他來(lái)做數(shù)據(jù)源再好不過(guò)。

1. 還是先從數(shù)據(jù)源定義開(kāi)始

func sectionableData() -> BehaviorRelay<[MGSection<MGItem>]> {
    let item1 = MGItem(str: "1")
    let item2 = MGItem(str: "2")
    let item3 = MGItem(str: "4")
    let item4 = MGItem(str: "5")

    let section1 = MGSection(header: "header1", items: [item1, item2])
    let section2 = MGSection(header: "header2", items: [item3, item4])

    return BehaviorRelay(value: [section1, section2])

}

我們用這個(gè)方法來(lái)創(chuàng)建一個(gè)數(shù)據(jù)源,MGSection可能有點(diǎn)陌生,其實(shí)就是我們現(xiàn)在需要的是帶Section的列表,而SectionHeader就是header1,header2

由于Rx本身并沒(méi)有MVVM的數(shù)據(jù)驅(qū)動(dòng)的概念,所以我們需要引入RxDataSouces這個(gè)神秘的庫(kù)來(lái)幫我們完善數(shù)據(jù)源,SectionModelType就是RxDataSouces中定義的協(xié)議了

/// MGSection model
public class MGSection<ItemElement>: SectionModelType {

    public typealias Item = ItemElement

    public var header: String = ""

    public var items: [Item] = []

    init() {

    }

    public required init(original: MGSection, items: [Item]) {
        self.header = original.header
        self.items = items
    }

    /// 初始化調(diào)用我就行了
    ///
    /// - Parameters:
    ///   - header: header string
    ///   - items: items
    public convenience init(header: String, items: [Item]) {
        let section = MGSection<Item>()
        section.header = header
        section.items = items

        self.init(original: section, items: items)
        self.header = header

    }
}

其實(shí)看不懂也無(wú)所謂,簡(jiǎn)單理解就是我們創(chuàng)建一個(gè)普通的數(shù)組,然后用RxDataSouces所要求的格式進(jìn)行封裝后得到了BehaviorRelay<[MGSection<MGItem>]>這么一個(gè)數(shù)據(jù)源,僅此而已。

2. Controller的綁定

override func viewDidLoad() {
    super.viewDidLoad()

    viewModel.initial()
    
    viewModel
        .sectionedData.asObservable()
        .bind(to: tableView, by: { (_, _, _, item) -> UITableViewCell in
            let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")
            cell?.textLabel?.text = item.name
            return cell!
        }).disposed(by: disposeBag)

}

這樣就看得懂了,雖然這個(gè)bind方法有點(diǎn)讓人無(wú)法理解,畢竟這是個(gè)封裝,內(nèi)容在這里:

func bind<RowItem>(to tableView: UITableView, by configCell : @escaping
    (TableViewSectionedDataSource<MGSection<RowItem>>,
    UITableView,
    IndexPath,
    RowItem) -> UITableViewCell )
    -> Disposable
    where E == DataSourceWithRequest<RowItem> {
        
        let realDataSource = RxTableViewSectionedReloadDataSource<MGSection<RowItem>>(configureCell: configCell)
        
        realDataSource.titleForHeaderInSection = { ds, index in
            return ds.sectionModels[index].header
        }
        
        return self.bind(to: tableView.rx.items(dataSource: realDataSource))
}

emmm... 怎么說(shuō)呢,這已經(jīng)是我能提供的最能看得懂的版本了,還是那句話,這里不多講Rx,所以最淺顯的理解就是我們把 RxDataSources封裝好的數(shù)據(jù)源bind給了tableView,畢竟bind這個(gè)動(dòng)作之前已經(jīng)見(jiàn)過(guò)了

image

大概樣子就是這樣,這也太簡(jiǎn)單了,所以接下來(lái)要給Add Item 按鈕搞點(diǎn)事情了

3. 改下數(shù)據(jù)源會(huì)怎么樣

 addBar.rx.tap.map{}.subscribe(onNext: { (_) in
       print("")
       self.viewModel.sectionedData.accept([])
 })

我們粗暴的用上面這個(gè)姿勢(shì)來(lái)修改,.accept其實(shí)就是修改了你母親大人腦中的貓糧,當(dāng)然這里是把貓糧吃完了~,而僅僅這一步操作后,其實(shí)tableView就已經(jīng)刷新了,即一個(gè)cell都木有了。

所以這就是我們要達(dá)到的目的,與之前MVC中把數(shù)據(jù)源清空后再tableView.reloadData()的操作相比,這樣更純粹,而之前其實(shí)我們即操作了數(shù)據(jù)源,又刷新了tableview,為了做刪除數(shù)據(jù)這個(gè)操作,我們改了2個(gè)部分的代碼,俗話說(shuō)得好,改的多錯(cuò)的越多~

當(dāng)然有人會(huì)說(shuō)其實(shí)這個(gè)修改數(shù)據(jù)源后tableview直接變的底層還是這么回事,那我只能說(shuō),畢竟那不是我們自己改的,錯(cuò)的不可能是我們~

當(dāng)然有個(gè)不怎么優(yōu)雅的點(diǎn)大家應(yīng)該可以發(fā)現(xiàn),說(shuō)是說(shuō)清空數(shù)據(jù),其實(shí)我們做的并不是removeAll的操作,而是新建了個(gè)空數(shù)組,換句話說(shuō)這時(shí)候的指令并不是告訴你母親大人請(qǐng)把貓糧吃完,我現(xiàn)在不想要了;而是我們直接給她一個(gè)空袋子說(shuō),你手上的貓糧吃完了,這種看似騙自己的手法確實(shí)有點(diǎn)尷尬,而由于BehaviorRelay中存儲(chǔ)的能量是不可編輯的,所以我們只有通過(guò)覆蓋原本的數(shù)組,來(lái)達(dá)到所謂的刪除目的。

所以如果是一個(gè)添加動(dòng)作,原本是[1,2],我們只要給個(gè)新數(shù)組[1,2,3],tableView做的其實(shí)就是insertSection動(dòng)作,至于為什么,RxDataSources做了你想知道的一切,這里就不多擴(kuò)展了。

4. MVVM一點(diǎn)呢?

之前提到過(guò)MVVM會(huì)讓簡(jiǎn)單事情復(fù)雜化,所以上面這個(gè)例子明顯并不MVVM,畢竟你怎么可以直接在按鈕點(diǎn)擊事件中去操作ViewModel中的業(yè)務(wù)數(shù)據(jù)呢?說(shuō)好的節(jié)點(diǎn)和快遞呢?

這種來(lái)自靈魂深處的拷問(wèn)讓我們無(wú)法作答,所以還是往下看吧。

addBar.rx.tap.map{}.bind(to: viewModel.addObjInput)

所以我們把Controller改成了這樣,

同時(shí)為了看著美觀,我們給BehaviorRelay加一個(gè)add方法來(lái)欺騙欺騙我們自己:

extension BehaviorRelay
{
    func add<T>(element: T) where Element == [T]
    {
        var newValue = self.value
        newValue.append(element)
        self.accept(newValue)
    }
}

上面提到過(guò)其實(shí)我們是用替換來(lái)充當(dāng)add,所以這里就露骨點(diǎn)這樣寫了,雖然外面調(diào)用的時(shí)候我們可以心安理得的用add了。

所以在ViewModel中,最終節(jié)點(diǎn)訂閱會(huì)變成:

    addObjInput.subscribe(onNext: { (_) in
        let item5 = MGItem(str: "5")
        let item6 = MGItem(str: "6")
        
        let section1 = MGSection(header: "header3", items: [item5, item6])
        self.sectionedData.add(element: section1)
    })

g結(jié)果就是這樣,你會(huì)問(wèn)既然是add那為什么沒(méi)動(dòng)畫,請(qǐng)相信我,這真的是add,只是我調(diào)了個(gè)沒(méi)動(dòng)畫的RxDatasource的封裝,僅此而已。

image

至此,你會(huì)發(fā)現(xiàn)我們改了數(shù)據(jù)源,tableView就出現(xiàn)了變化,這就是MVVM的另一個(gè)核心。


場(chǎng)景三:簡(jiǎn)單的不能簡(jiǎn)單的計(jì)數(shù)label

image

圖中的需求其實(shí)很簡(jiǎn)單,你會(huì)說(shuō)button點(diǎn)擊時(shí)候改變?nèi)肿兞烤托辛耍俳olabel刷新一下就完了,但是你發(fā)現(xiàn)其實(shí)你又操作了數(shù)據(jù),又操作了label,仿佛又有些不太對(duì),
因?yàn)檫@個(gè)場(chǎng)景只是個(gè)鞏固,所以我們就直接貼全部代碼了:
Controller:

class ViewController: UIViewController {

    //MARK : - UIs
    @IBOutlet weak var countLabel: UILabel!
    
    @IBOutlet weak var countButton: UIButton!


    let disposeBag = DisposeBag()

     let vm = ViewModel()

    //MARK : - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.counter.map { "\($0)" }.bind(to: countLabel.rx.text)
        countButton.rx.tap.bind(to: viewModel.input)
    }
}

ViewModel:

class ViewModel {
    let input: PublishRelay<Void> = PublishRelay()

    let counter: BehaviorRelay<Int> = BehaviorRelay(value: 1)
    
    init() {
        input.map { _ in self.counter.value + 1 }.bind(to: counter)
    }
}

同樣我們把所有事情移到了ViewModel,同樣我們省去了刷新Label這么一個(gè)操作,綁定配置之后,我們就可以安心去對(duì)我們的ViewModel做所有業(yè)務(wù)操作了,其實(shí)嚴(yán)格意義上來(lái)說(shuō){ "\($0)" }這個(gè)操作也應(yīng)該在ViewModel中,奈何實(shí)在是不想多寫了,我就寫了個(gè)反面教材以示警戒吧~

至此,簡(jiǎn)單的入門應(yīng)該真的入了,如果還沒(méi)有,那就只能留言作者或者強(qiáng)行懟作者了~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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