RxSwift-MVVM-初体验

48次阅读

共计 4674 个字符,预计需要花费 12 分钟才能阅读完成。

一、原起

作为一名 iOS 开发者,必须跟上时代的潮流,随着 swift ABI越来越稳定,使用 swift 开发 iOS APP 的人越来越多。从网上看了很多文章,也从 github 上下载了很多 demo 进行代码学习。最近使用 RxSwift+MVVM+Moya 进行了 swift 的体验之旅。加入到 swift 开发的大潮中去。

二、目录结构

这个 demo 的项目结构包括:ViewModelViewModelControllerToolExtension

ViewModelMVVM 架构模式与 MVC 架构模式最大的区别点。MVVM 架构模式把业务逻辑从 controller 集中到了 ViewModel 中,方便进行 单元测试 自动化测试

ViewModel 的业务模型如下:

viewmodel 相当于是一个黑盒子,封装了业务逻辑,进行输入和输出的转换。
其中 ViewModelMVC架构模式下负责的任务相同。controller由于业务逻辑移到了 Viewmodel 中,它本身担起了 中间调用者 角色,负责把 ViewViewmodel绑定在一起。

demo 的整体目录结构如下:

三、使用到的第三方库

开发一个 App 最基本的三大要素:网络请求 数据解析UI 布局,其它的都是这三大要素相关联的,或者更细的功能划分。

  • 网络请求库使用的Moya
  • 数据解析使用的是ObjectMapper
  • UI 布局使用的是自动布局框架Snapkit
  • 图片加载和缓存使用的是Kingfisher
  • 刷新组件使用的MJRefresh
  • 网络加载提示使用的是SVProgressHUD

使用到的三方库的 cocoapod 目录如下:

四、具体实现

4.1 viewmodel 的协议

viewmodel 的实现需要继承 NJWViewModelType 这个协议,需要实现 输入 -> 输出 这个方法。这个算是 viewmodel 的一个基本范式吧。

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

4.2 viewmodel 的具体实现

这里包括了 输入 输出 的具体实现,与及 func transform(input: NJWViewModel.NJWInput) -> NJWViewModel.NJWOutput 这个输入转输出方法具体的实现逻辑。具体代码如下:

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 中数据绑定的具体实现

输入 输出 collectionview进行绑定,建立联系,达到操作 UI 进行数据刷新的目的。具体的绑定逻辑如下:

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)
        })
    }

五、效果展示如下

六、demo 地址

没有 demo 的文章不是好文章,demo 的传送门。

正文完
 0