一、原起
作为一名iOS开发者,必须跟上时代的潮流,随着swift ABI越来越稳定,使用swift开发iOS APP 的人越来越多。从网上看了很多文章,也从github上下载了很多demo进行代码学习。最近使用RxSwift+MVVM+Moya进行了swift的体验之旅。加入到swift开发的大潮中去。
二、目录结构
这个demo的项目结构包括:View、Model、ViewModel、Controller、Tool、Extension。
ViewModel是MVVM架构模式与MVC架构模式最大的区别点。MVVM架构模式把业务逻辑从controller集中到了ViewModel中,方便进行单元测试和自动化测试。
ViewModel的业务模型如下:
viewmodel相当于是一个黑盒子,封装了业务逻辑,进行输入和输出的转换。
其中View、Model与MVC架构模式下负责的任务相同。controller由于业务逻辑移到了Viewmodel中,它本身担起了中间调用者角色,负责把View和Viewmodel绑定在一起。
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的传送门。