RxSwift + MVVM 初體驗(yàn)

一、原起

作為一名iOS開(kāi)發(fā)者,必須跟上時(shí)代的潮流,隨著swift ABI越來(lái)越穩(wěn)定,使用swift開(kāi)發(fā)iOS APP 的人越來(lái)越多。從網(wǎng)上看了很多文章,也從github上下載了很多demo進(jìn)行代碼學(xué)習(xí)。最近使用RxSwift+MVVM+Moya進(jìn)行了swift的體驗(yàn)之旅。加入到swift開(kāi)發(fā)的大潮中去。

二、目錄結(jié)構(gòu)

這個(gè)demo的項(xiàng)目結(jié)構(gòu)包括:ViewModel、ViewModelController、Tool、Extension。

ViewModelMVVM架構(gòu)模式與MVC架構(gòu)模式最大的區(qū)別點(diǎn)。MVVM架構(gòu)模式把業(yè)務(wù)邏輯從controller集中到了ViewModel中,方便進(jìn)行單元測(cè)試自動(dòng)化測(cè)試。

ViewModel的業(yè)務(wù)模型如下:

ViewModel模型

viewmodel相當(dāng)于是一個(gè)黑盒子,封裝了業(yè)務(wù)邏輯,進(jìn)行輸入和輸出的轉(zhuǎn)換。
其中View、ModelMVC架構(gòu)模式下負(fù)責(zé)的任務(wù)相同。controller由于業(yè)務(wù)邏輯移到了Viewmodel中,它本身?yè)?dān)起了中間調(diào)用者角色,負(fù)責(zé)把ViewViewmodel綁定在一起。

demo的整體目錄結(jié)構(gòu)如下:


groups

三、使用到的第三方庫(kù)

開(kāi)發(fā)一個(gè)App最基本的三大要素:網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)解析UI布局,其它的都是這三大要素相關(guān)聯(lián)的,或者更細(xì)的功能劃分。

  • 網(wǎng)絡(luò)請(qǐng)求庫(kù)使用的Moya
  • 數(shù)據(jù)解析使用的是ObjectMapper
  • UI布局使用的是自動(dòng)布局框架Snapkit,
  • 圖片加載和緩存使用的是Kingfisher,
  • 刷新組件使用的MJRefresh,
  • 網(wǎng)絡(luò)加載提示使用的是SVProgressHUD。

使用到的三方庫(kù)的cocoapod目錄如下:


cocoapods

四、具體實(shí)現(xiàn)

4.1 viewmodel的協(xié)議

viewmodel的實(shí)現(xiàn)需要繼承NJWViewModelType這個(gè)協(xié)議,需要實(shí)現(xiàn)輸入->輸出這個(gè)方法。這個(gè)算是viewmodel的一個(gè)基本范式吧。

protocol NJWViewModelType {
    associatedtype Input
    associatedtype Output
    
    func transform(input: Input) -> Output
}

4.2 viewmodel的具體實(shí)現(xiàn)

這里包括了輸入、輸出的具體實(shí)現(xiàn),與及func transform(input: NJWViewModel.NJWInput) -> NJWViewModel.NJWOutput這個(gè)輸入轉(zhuǎn)輸出方法具體的實(shí)現(xiàn)邏輯。具體代碼如下:

class NJWViewModel: NSObject {
    
    let models = Variable<[GirlModel]>([])
    var index: Int = 0
}

extension NJWViewModel: NJWViewModelType{
    
    typealias Input = NJWInput
    typealias Output = NJWOutput
    
    struct NJWInput {
        
        var category = BehaviorRelay<ApiManager.GirlCategory>(value: .GirlCategoryAll)
        init(category: BehaviorRelay<ApiManager.GirlCategory>) {
            self.category = category
        }
    }
    
    struct NJWOutput {
        
        let sections: Driver<[NJWSection]>
        let requestCommand = PublishSubject<Bool>()
        let refreshStatus = Variable<NJWRefreshStatus>(.none)
        
        init(sections: Driver<[NJWSection]>) {
            self.sections = sections
        }
    }
    
    func transform(input: NJWViewModel.NJWInput) -> NJWViewModel.NJWOutput {
        let sections = models.asObservable().map{ (models) -> [NJWSection] in
            return [NJWSection(items: models)]
        }.asDriver(onErrorJustReturn: [])
        
        let output = Output(sections: sections)
        input.category.asObservable().subscribe{
          
            let category = $0.element
            
            output.requestCommand.subscribe(onNext: { [unowned self] isReloadData in
                self.index = isReloadData ? 0 : self.index + 1
                NJWNetTool.rx.request(.requestWithcategory(type: category!, index: self.index))
                    .asObservable()
                    .mapArray(GirlModel.self)
                    .subscribe({[weak self] (event) in
                        switch event{
                            
                        case let .next(modelArr):
                            self?.models.value = isReloadData ? modelArr : (self?.models.value ?? []) + modelArr
                            NJWProgressHUD.showSuccess("加載成功")
                        case let .error(error):
                            NJWProgressHUD.showError(error.localizedDescription)
                        case .completed:
                            output.refreshStatus.value = isReloadData ? NJWRefreshStatus.endHeaderRefresh : NJWRefreshStatus.endFooterRefresh
                        }
                    }).disposed(by: self.rx.disposeBag)
            }).disposed(by: self.rx.disposeBag)
            
        }.disposed(by: rx.disposeBag)
        
        return output
    }
}

4.3 controller中數(shù)據(jù)綁定的具體實(shí)現(xiàn)

輸入、輸出collectionview進(jìn)行綁定,建立聯(lián)系,達(dá)到操作UI進(jìn)行數(shù)據(jù)刷新的目的。具體的綁定邏輯如下:

fileprivate func bindView(){
        
        let vmInput = NJWViewModel.NJWInput(category: self.category)
        let vmOutput = viewModel.transform(input: vmInput)
        vmOutput.sections.asDriver().drive(collectionView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag)
        vmOutput.refreshStatus.asObservable().subscribe(onNext: {[weak self] status in
            switch status {
            case .beingHeaderRefresh:
                self?.collectionView.mj_header.beginRefreshing()
            case .endHeaderRefresh:
                self?.collectionView.mj_header.endRefreshing()
            case .beingFooterRefresh:
                self?.collectionView.mj_footer.beginRefreshing()
            case .endFooterRefresh:
                self?.collectionView.mj_footer.endRefreshing()
            case .noMoreData:
                self?.collectionView.mj_footer.endRefreshingWithNoMoreData()
            default:
                break
            }
        }).disposed(by: rx.disposeBag)
        
//        Observable.zip(collectionView.rx.itemSelected, collectionView.rx.modelSelected(GirlModel.self)).bind(onNext: {[weak self] indexPath, itemModel in
//            var phtoUrlArray: Array<String> = []
//            phtoUrlArray.append(itemModel.image_url)
//            let photoBrowser: SYPhotoBrowser = SYPhotoBrowser(imageSourceArray: phtoUrlArray, caption: nil, delegate: self)
////                photoBrowser.prefersStatusBarHidden = false
////            photoBrowser.pageControlStyle = SYPhotoBrowserPageControlStyle
//            photoBrowser.initialPageIndex = UInt(indexPath.item)
//            UIApplication.shared.delegate?.window?!.rootViewController?.present(photoBrowser, animated: true)
//        }).disposed(by: disposeBag)
        collectionView.rx.modelSelected(GirlModel.self).subscribe(onNext:{[weak self] itemModel in

            print("current selected model is \(itemModel)")
            let photoBrowser: SYPhotoBrowser = SYPhotoBrowser(imageSourceArray: [itemModel.image_url], caption: nil, delegate: self)
            //                photoBrowser.prefersStatusBarHidden = false
            //            photoBrowser.pageControlStyle = SYPhotoBrowserPageControlStyle
            UIApplication.shared.delegate?.window?!.rootViewController?.present(photoBrowser, animated: true)

        }).disposed(by: disposeBag)
        
        collectionView.mj_header = MJRefreshNormalHeader(refreshingBlock: {
            vmOutput.requestCommand.onNext(true)
//            self.collectionView.reloadData()
        })
        
        collectionView.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: {
            vmOutput.requestCommand.onNext(false)
        })
    }

五、效果展示如下

effect

六、demo地址

沒(méi)有demo的文章不是好文章,demo的傳送門(mén)。

?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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