一、原起
作为一名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的传送门。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。