一、原起
作為一名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)包括:View、Model、ViewModel、Controller、Tool、Extension。
ViewModel是MVVM架構(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相當(dāng)于是一個(gè)黑盒子,封裝了業(yè)務(wù)邏輯,進(jìn)行輸入和輸出的轉(zhuǎn)換。
其中View、Model與MVC架構(gòu)模式下負(fù)責(zé)的任務(wù)相同。controller由于業(yè)務(wù)邏輯移到了Viewmodel中,它本身?yè)?dān)起了中間調(diào)用者角色,負(fù)責(zé)把View和Viewmodel綁定在一起。
demo的整體目錄結(jié)構(gòu)如下:

三、使用到的第三方庫(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目錄如下:

四、具體實(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)
})
}
五、效果展示如下

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