今天我們將用RxSwift加上MVVM設(shè)計(jì)模式來(lái)開(kāi)發(fā)一個(gè)簡(jiǎn)單的小Demo德迹,在UICollectionView和UITableView中顯示林肯公園的專(zhuān)輯和歌曲列表。Let's go!
UI 部分
第一步双吆,用CollectionView搭建專(zhuān)輯九宮格,用TableView搭建歌曲列表会前。
這兩部分我們可以分成兩個(gè)控制器來(lái)做好乐,主要為了能重復(fù)利用,然后使用 childViewController 來(lái)添加瓦宜。
我們的主控制器就劃分為兩個(gè)控制器:
- AlbumCollectionViewVC
- TrackTableViewVC
所以我們的主控制器就會(huì)像這樣:
第二步蔚万,使用nib創(chuàng)建cells,以便后面能夠重用它們:
記得在AlbumCollectionViewVC
的viewDidLoad
方法中注冊(cè)nib文件:
//register 'AlbumsCollectionViewCell' to UICollectionView
albumsCollectionView.register(UINib(nibName: "AlbumsCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: String(describing: AlbumsCollectionViewCell.self))
第三步临庇,關(guān)聯(lián)兩個(gè)view作為AlbumCollectionViewVC和TrackTableViewVC的容器view反璃。
@IBOutlet weak var albumsVCView: UIView!
private lazy var albumsViewController: AlbumsCollectionViewVC = {
// Load Storyboard
let storyboard = UIStoryboard(name: "Home", bundle: Bundle.main)
// Instantiate View Controller
var viewController = storyboard.instantiateViewController(withIdentifier: "AlbumsCollectionViewVC") as! AlbumsCollectionViewVC
// Add View Controller as Child View Controller
self.add(asChildViewController: viewController, to: albumsVCView)
return viewController
}()
我們已經(jīng)把視圖部分搭建好了,接下來(lái)創(chuàng)建ViewModel假夺。
View Model 部分
我們創(chuàng)建一個(gè)HomeViewModel類(lèi)淮蜈,在該類(lèi)的作用是:
- 從服務(wù)器獲取數(shù)據(jù),并按照視圖需要展現(xiàn)的方式來(lái)解析請(qǐng)求到的數(shù)據(jù)已卷。
- 將解析到的數(shù)據(jù)傳遞給父類(lèi)控制器梧田,父類(lèi)控制器將這些數(shù)據(jù)再傳遞給子視圖控制器。
為了更好地理解,請(qǐng)看下面的圖表:
所以整個(gè)過(guò)程就是:父類(lèi)控制器從它的視圖模型請(qǐng)求數(shù)據(jù)柿扣,視圖模型向網(wǎng)絡(luò)層發(fā)送請(qǐng)求肖方。然后視圖模型解析數(shù)據(jù)并將其提供給父類(lèi)控制器。
HomeViewModel中還提供了以下幾個(gè)屬性:
- Loading(Bool):當(dāng)我們向服務(wù)器發(fā)送請(qǐng)求時(shí)未状,我們應(yīng)該顯示Loading提示彈框以示加載中俯画。這樣用戶(hù)就明白,有些東西正在加載司草。為此艰垂,我們需要定義一個(gè)
PublishSubject<Bool>
類(lèi)型的可觀察對(duì)象。當(dāng)它為true時(shí)埋虹,它將意味著正在加載猜憎,當(dāng)它為false時(shí),意味著已經(jīng)加載完成搔课。 - Error(homeError):來(lái)自服務(wù)器的錯(cuò)誤和任何其他錯(cuò)誤胰柑。如果它有值,我們將它在屏幕上顯示出來(lái)爬泥。
- albums和tracks:集合和表視圖數(shù)據(jù)柬讨。
public enum HomeError {
case internetError(String)
case serverMessage(String)
}
public let albums : PublishSubject<[Album]> = PublishSubject()
public let tracks : PublishSubject<[Track]> = PublishSubject()
public let loading: PublishSubject<Bool> = PublishSubject()
public let error : PublishSubject<HomeError> = PublishSubject()
上面四個(gè)屬性都是用PublishSubject定義的observable對(duì)象,問(wèn)題來(lái)了袍啡,PublishSubject是什么踩官,怎么用?
Subjects 和 PublishSubject
再了解PublishSubject之前境输,先來(lái)聊聊Subjects蔗牡。
Subjects既是可觀察的observable對(duì)象又是observer觀察者。它們可以接收事件嗅剖,也可以訂閱辩越。subject接收.next事件,并且每次接收到一個(gè)事件信粮,它都會(huì)返回并將其發(fā)送給它的訂閱者黔攒。
RxSwift有4種subject類(lèi)型:
- PublishSubject。
- BehaviorSubject蒋院。
- ReplaySubject。
- Variable莲绰。
PublishSubject是Subject的一種欺旧。我們用一張圖來(lái)展示一下:
從圖中看出,如果在事件1之后訂閱蛤签,我們就不能接收到事件1辞友,而可以接收到事件2和事件3。如果在事件2之后訂閱,那么我們只能接收到事件3称龙,而接收不到事件1和事件2留拾。
PublishSubject 如何使用?
我們來(lái)舉個(gè)例子鲫尊。
第一步痴柔,創(chuàng)建一個(gè)類(lèi)型為String的PublishSubject:
let subject = PublishSubject<String>()
第二步,通過(guò)onNext發(fā)出事件:
subject.onNext(“No event emitted??”)
此時(shí)不會(huì)有任何打印疫向,因?yàn)闆](méi)有人訂閱這個(gè)它咳蔚。
第三步,我們來(lái)訂閱它:
let subscriptionOne = subject
.subscribe(onNext: { string in
print("First Subscription: ", string)
})
第四步搔驼,使用subject通過(guò)onNext發(fā)出事件:
subject.onNext("1")
subject.onNext("2")
只輸出了訂閱后的事件數(shù)據(jù):
First Subscription: 1
First Subscription: 2
第五步谈火,再創(chuàng)建另一個(gè)訂閱:
let subscriptionTwo = subject
.subscribe({ (event) in
print("Second Subscription: \(event)"))
})
第六步,并發(fā)射事件:
subject.onNext("3")
這是在subscription1和subscription2被發(fā)出后訂閱的舌涨,所以此時(shí)的訂閱者只能監(jiān)聽(tīng)到事件3糯耍,又增加了以下的打印的結(jié)果:
First Subscription: 3
Second Subscription: next(3)
第七步,我們嘗試釋放掉subscriptionOne并發(fā)出事件4:
subscriptionOne.dispose()
subject.onNext("4")
這時(shí)只有觀察者subscriptionTwo可以監(jiān)聽(tīng)事件囊嘉,并執(zhí)行打印操作温技,因?yàn)閟ubscriptionOne資源已被釋放。
Second Subscription: next(4)
第八步哗伯,嘗試發(fā)出complete event荒揣,同時(shí)釋放掉subscriptionTwo。
subject.onCompleted()
subscriptionTwo.dispose()
subject.onNext("Any event emitted??")
Complete event 會(huì)被打印出來(lái):
Second Subscription: completed
整個(gè)代碼如下:
let subject = PublishSubject<String>()
subject.onNext(“No event emitted??”)
let subscriptionOne = subject
.subscribe(onNext: { string in
print("First Subscription: ", string)
})
subject.onNext("1")
subject.onNext("2")
let subscriptionTwo = subject
.subscribe({ (event) in
print("Second Subscription: \(event)"))
})
subject.onNext("3")
subscriptionOne.dispose()
subject.onNext("4")
subject.onCompleted()
subscriptionTwo.dispose()
subject.onNext("Any event emitted??")
整個(gè)輸出如下:
First Subscription: 1
First Subscription: 2
First Subscription: 3
Second Subscription: next(3)
Second Subscription: next(4)
Second Subscription: completed
PublishSubject相當(dāng)于熱信號(hào)焊刹,只會(huì)接收到后面的事件系任。我們這個(gè)Demo只會(huì)用到PublishSubject,也是因?yàn)樗某跏蓟挥媒o初始值虐块,比較方便俩滥。
將數(shù)據(jù)綁定到UI上
現(xiàn)在,我們來(lái)看看如何給我們的視圖提供數(shù)據(jù)贺奠。
首先霜旧,我們將homeViewModel中的loading綁定給HomeVC 的isAnimating
,這意味著每當(dāng)viewModel的loading值發(fā)生改變時(shí)儡率,視圖控制器的isAnimating
值也會(huì)同時(shí)改變挂据。
homeViewModel.loading
.bind(to: self.rx.isAnimating).disposed(by: disposeBag)
看到上面的.rx
了嗎?類(lèi)似的我們可以通過(guò).rx
的形式訪(fǎng)問(wèn)到很多視圖的屬性儿普,從而能夠?qū)?shù)據(jù)綁定到UIKit中崎逃。
但是要注意了,對(duì)于自定義的屬性眉孩,RxCocoa并不支持.rx
个绍,我們可以使用拓展來(lái)讓它支持:
extension Reactive where Base: UIViewController {
/// Bindable sink for `startAnimating()`, `stopAnimating()` methods.
public var isAnimating: Binder<Bool> {
return Binder(self.base, binding: { (vc, active) in
if active {
vc.startAnimating()
} else {
vc.stopAnimating()
}
})
}
}
我解釋一下上面的代碼:
- 對(duì)Reactive進(jìn)行了擴(kuò)展勒葱,目的想對(duì)自定義的屬性進(jìn)行
.rx
調(diào)用。 - 設(shè)置isAnimating變量的類(lèi)型為Binder<Bool>巴柿,并對(duì)它進(jìn)行實(shí)現(xiàn)凛虽。
- 實(shí)現(xiàn)代碼里,返回了一個(gè)Binder广恢,self.base就是viewController本身凯旋,后面的閉包提供兩個(gè)參數(shù):視圖控制器(vc)和isAnimating值(active)。如果active為true袁波,我們就用
vc.startAnimating()
顯示正在加載動(dòng)畫(huà)瓦阐,如果active為false,就用vc.stopAnimating()
隱藏加載動(dòng)畫(huà)篷牌。
現(xiàn)在我們的loading已經(jīng)準(zhǔn)備好了接收來(lái)自ViewModel的數(shù)據(jù)睡蟋。讓我們來(lái)看看其他的binders:
// observing errors to show
homeViewModel
.error
.observeOn(MainScheduler.instance)
.subscribe(onNext: { (error) in
switch error {
case .internetError(let message):
MessageView.sharedInstance.showOnView(message: message, theme: .error)
case .serverMessage(let message):
MessageView.sharedInstance.showOnView(message: message, theme: .warning)
}
})
.disposed(by: disposeBag)
在上面的代碼中,每當(dāng)有來(lái)自ViewModel的錯(cuò)誤出現(xiàn)時(shí)枷颊,我們都會(huì)訂閱到它戳杀。我們可以對(duì)監(jiān)聽(tīng)到的錯(cuò)誤進(jìn)行進(jìn)一步處理,比如顯示一個(gè)彈出窗口什么的夭苗。
那么.observeOn(MainScheduler.instance)又是什么呢信卡?它的作用是回到主線(xiàn)程發(fā)送錯(cuò)誤信號(hào)。
綁定Albums 和 Tracks
現(xiàn)在我們將Albums 和 Tracks分別綁定給UICollectionView和UITableView:
// binding albums to album container
homeViewModel
.albums
.observeOn(MainScheduler.instance)
.bind(to: albumsViewController.albums)
.disposed(by: disposeBag)
// binding tracks to track container
homeViewModel
.tracks
.observeOn(MainScheduler.instance)
.bind(to: tracksViewController.tracks)
.disposed(by: disposeBag)
數(shù)據(jù)請(qǐng)求
現(xiàn)在我們?cè)倩氐絍iewModel中题造,編寫(xiě)數(shù)據(jù)請(qǐng)求部分:
public func requestData(){
self.loading.onNext(true)
APIManager.requestData(url: requestUrl, method: .get, parameters: nil, completion: { (result) in
self.loading.onNext(false)
switch result {
case .success(let returnJson) :
let albums = returnJson["Albums"].arrayValue.compactMap {return Album(data: try! $0.rawData())}
let tracks = returnJson["Tracks"].arrayValue.compactMap {return Track(data: try! $0.rawData())}
self.albums.onNext(albums)
self.tracks.onNext(tracks)
case .failure(let failure) :
switch failure {
case .connectionError:
self.error.onNext(.internetError("Check your Internet connection."))
case .authorizationError(let errorJson):
self.error.onNext(.serverMessage(errorJson["message"].stringValue))
default:
self.error.onNext(.serverMessage("Unknown Error"))
}
}
})
}
- 首先我們對(duì)loading發(fā)送一個(gè)true值傍菇,因?yàn)槲覀円呀?jīng)在HomeVC類(lèi)中綁定了loading,這樣我們的viewController就會(huì)顯示正在加載動(dòng)畫(huà)界赔。
- 接下來(lái)丢习,發(fā)送一個(gè)數(shù)據(jù)請(qǐng)求,得到響應(yīng)后淮悼,在回調(diào)閉包里我們應(yīng)該結(jié)束加載動(dòng)畫(huà)咐低,所以又發(fā)送了一個(gè)false值。
- 對(duì)于請(qǐng)求結(jié)果result我們做了區(qū)分袜腥,如果為success见擦,就解析數(shù)據(jù)并發(fā)出albums和albums的值,如果為false羹令,就會(huì)發(fā)出錯(cuò)誤值鲤屡,根據(jù)錯(cuò)誤不同的情況我們?cè)僮鱿鄳?yīng)的處理。
現(xiàn)在我們的數(shù)據(jù)準(zhǔn)備好了福侈,我們傳遞給了我們的子視圖控制器酒来,最后我們應(yīng)該在CollectionView和TableView中顯示數(shù)據(jù)了。
TracksTableViewVC中:
public var tracks = PublishSubject<[Track]>()
AlbumsCollectionViewVC中:
public var albums = PublishSubject<[Album]>()
現(xiàn)在在trackTableViewVC的ViewDidLoad
方法里癌刽,將數(shù)據(jù)源tracks綁定給UITableView:
tracks.bind(to: tracksTableView.rx.items(cellIdentifier: "TracksTableViewCell", cellType: TracksTableViewCell.self)) { (row,track,cell) in
cell.cellTrack = track
}.disposed(by: disposeBag)
僅僅兩行代碼就搞定了役首,不需要想以前那樣設(shè)置delegate和datasource,也不用寫(xiě)一大堆代理方法显拜,RxCocoa搞定了一切衡奥!
public var cellTrack : Track! {
didSet {
self.trackImage.clipsToBounds = true
self.trackImage.layer.cornerRadius = 3
self.trackImage.loadImage(fromURL: cellTrack.trackArtWork)
self.trackTitle.text = cellTrack.name
self.trackArtist.text = cellTrack.artist
}
}
給tableView 和 collectionView 添加動(dòng)畫(huà):
// animation to cells
tracksTableView.rx.willDisplayCell
.subscribe(onNext: ({ (cell,indexPath) in
cell.alpha = 0
let transform = CATransform3DTranslate(CATransform3DIdentity, -250, 0, 0)
cell.layer.transform = transform
UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: .curveEaseOut, animations: {
cell.alpha = 1
cell.layer.transform = CATransform3DIdentity
}, completion: nil)
})).disposed(by: disposeBag)
最后完成效果:
小結(jié)
我們使用RxSwift和MVVM實(shí)現(xiàn)了一個(gè)小Demo,其中只是涉及到部分概念远荠,后續(xù)會(huì)通過(guò)實(shí)踐來(lái)學(xué)習(xí)其他的用法矮固。