一现恼、原起
作為一名iOS開發(fā)者肃续,必須跟上時代的潮流,隨著swift ABI越來越穩(wěn)定叉袍,使用swift開發(fā)iOS APP 的人越來越多始锚。從網(wǎng)上看了很多文章,也從github上下載了很多demo進行代碼學習喳逛。最近使用RxSwift+MVVM+Moya進行了swift的體驗之旅瞧捌。加入到swift開發(fā)的大潮中去。
二润文、目錄結構
這個demo的項目結構包括:View姐呐、Model、ViewModel典蝌、Controller曙砂、Tool、Extension赠法。
ViewModel是MVVM架構模式與MVC架構模式最大的區(qū)別點麦轰。MVVM架構模式把業(yè)務邏輯從controller集中到了ViewModel中,方便進行單元測試和自動化測試砖织。
ViewModel的業(yè)務模型如下:
viewmodel相當于是一個黑盒子款侵,封裝了業(yè)務邏輯,進行輸入和輸出的轉換侧纯。
其中View新锈、Model與MVC架構模式下負責的任務相同。controller由于業(yè)務邏輯移到了Viewmodel中眶熬,它本身擔起了中間調用者角色妹笆,負責把View和Viewmodel綁定在一起块请。
demo的整體目錄結構如下:
三、使用到的第三方庫
開發(fā)一個App最基本的三大要素:網(wǎng)絡請求拳缠、數(shù)據(jù)解析墩新、UI布局,其它的都是這三大要素相關聯(lián)的窟坐,或者更細的功能劃分海渊。
- 網(wǎng)絡請求庫使用的Moya,
- 數(shù)據(jù)解析使用的是ObjectMapper哲鸳,
- UI布局使用的是自動布局框架Snapkit臣疑,
- 圖片加載和緩存使用的是Kingfisher,
- 刷新組件使用的MJRefresh徙菠,
- 網(wǎng)絡加載提示使用的是SVProgressHUD讯沈。
使用到的三方庫的cocoapod目錄如下:
四、具體實現(xiàn)
4.1 viewmodel的協(xié)議
viewmodel的實現(xiàn)需要繼承NJWViewModelType這個協(xié)議婿奔,需要實現(xiàn)輸入->輸出這個方法缺狠。這個算是viewmodel的一個基本范式吧。
protocol NJWViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
4.2 viewmodel的具體實現(xiàn)
這里包括了輸入萍摊、輸出的具體實現(xiàn)儒老,與及func transform(input: NJWViewModel.NJWInput) -> NJWViewModel.NJWOutput
這個輸入轉輸出方法具體的實現(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ù)綁定的具體實現(xiàn)
把輸入记餐、輸出和collectionview
進行綁定,建立聯(lián)系薇正,達到操作UI進行數(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地址
沒有demo的文章不是好文章雕沿,demo的傳送門。