今天我们将用RxSwift加上MVVM设计模式来开发一个简单的小Demo,在UICollectionView和UITableView中显示林肯公园的专辑和歌曲列表。Let's go!
UI 部分
第一步,用CollectionView搭建专辑九宫格,用TableView搭建歌曲列表。
这两部分我们可以分成两个控制器来做,主要为了能重复利用,然后使用 childViewController 来添加。
我们的主控制器就划分为两个控制器:
- AlbumCollectionViewVC
- TrackTableViewVC
所以我们的主控制器就会像这样:
第二步,使用nib创建cells,以便后面能够重用它们:
记得在AlbumCollectionViewVC
的viewDidLoad
方法中注册nib文件:
//register 'AlbumsCollectionViewCell' to UICollectionView
albumsCollectionView.register(UINib(nibName: "AlbumsCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: String(describing: AlbumsCollectionViewCell.self))
第三步,关联两个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
}()
我们已经把视图部分搭建好了,接下来创建ViewModel。
View Model 部分
我们创建一个HomeViewModel类,在该类的作用是:
- 从服务器获取数据,并按照视图需要展现的方式来解析请求到的数据。
- 将解析到的数据传递给父类控制器,父类控制器将这些数据再传递给子视图控制器。
为了更好地理解,请看下面的图表:
所以整个过程就是:父类控制器从它的视图模型请求数据,视图模型向网络层发送请求。然后视图模型解析数据并将其提供给父类控制器。
HomeViewModel中还提供了以下几个属性:
- Loading(Bool):当我们向服务器发送请求时,我们应该显示Loading提示弹框以示加载中。这样用户就明白,有些东西正在加载。为此,我们需要定义一个
PublishSubject<Bool>
类型的可观察对象。当它为true时,它将意味着正在加载,当它为false时,意味着已经加载完成。 - Error(homeError):来自服务器的错误和任何其他错误。如果它有值,我们将它在屏幕上显示出来。
- albums和tracks:集合和表视图数据。
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()
上面四个属性都是用PublishSubject定义的observable对象,问题来了,PublishSubject是什么,怎么用?
Subjects 和 PublishSubject
再了解PublishSubject之前,先来聊聊Subjects。
Subjects既是可观察的observable对象又是observer观察者。它们可以接收事件,也可以订阅。subject接收.next事件,并且每次接收到一个事件,它都会返回并将其发送给它的订阅者。
RxSwift有4种subject类型:
- PublishSubject。
- BehaviorSubject。
- ReplaySubject。
- Variable。
PublishSubject是Subject的一种。我们用一张图来展示一下:
从图中看出,如果在事件1之后订阅,我们就不能接收到事件1,而可以接收到事件2和事件3。如果在事件2之后订阅,那么我们只能接收到事件3,而接收不到事件1和事件2。
PublishSubject 如何使用?
我们来举个例子。
第一步,创建一个类型为String的PublishSubject:
let subject = PublishSubject<String>()
第二步,通过onNext发出事件:
subject.onNext(“No event emitted??”)
此时不会有任何打印,因为没有人订阅这个它。
第三步,我们来订阅它:
let subscriptionOne = subject
.subscribe(onNext: { string in
print("First Subscription: ", string)
})
第四步,使用subject通过onNext发出事件:
subject.onNext("1")
subject.onNext("2")
只输出了订阅后的事件数据:
First Subscription: 1
First Subscription: 2
第五步,再创建另一个订阅:
let subscriptionTwo = subject
.subscribe({ (event) in
print("Second Subscription: \(event)"))
})
第六步,并发射事件:
subject.onNext("3")
这是在subscription1和subscription2被发出后订阅的,所以此时的订阅者只能监听到事件3,又增加了以下的打印的结果:
First Subscription: 3
Second Subscription: next(3)
第七步,我们尝试释放掉subscriptionOne并发出事件4:
subscriptionOne.dispose()
subject.onNext("4")
这时只有观察者subscriptionTwo可以监听事件,并执行打印操作,因为subscriptionOne资源已被释放。
Second Subscription: next(4)
第八步,尝试发出complete event,同时释放掉subscriptionTwo。
subject.onCompleted()
subscriptionTwo.dispose()
subject.onNext("Any event emitted??")
Complete event 会被打印出来:
Second Subscription: completed
整个代码如下:
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??")
整个输出如下:
First Subscription: 1
First Subscription: 2
First Subscription: 3
Second Subscription: next(3)
Second Subscription: next(4)
Second Subscription: completed
PublishSubject相当于热信号,只会接收到后面的事件。我们这个Demo只会用到PublishSubject,也是因为它的初始化不用给初始值,比较方便。
将数据绑定到UI上
现在,我们来看看如何给我们的视图提供数据。
首先,我们将homeViewModel中的loading绑定给HomeVC 的isAnimating
,这意味着每当viewModel的loading值发生改变时,视图控制器的isAnimating
值也会同时改变。
homeViewModel.loading
.bind(to: self.rx.isAnimating).disposed(by: disposeBag)
看到上面的.rx
了吗?类似的我们可以通过.rx
的形式访问到很多视图的属性,从而能够将数据绑定到UIKit中。
但是要注意了,对于自定义的属性,RxCocoa并不支持.rx
,我们可以使用拓展来让它支持:
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()
}
})
}
}
我解释一下上面的代码:
- 对Reactive进行了扩展,目的想对自定义的属性进行
.rx
调用。 - 设置isAnimating变量的类型为Binder<Bool>,并对它进行实现。
- 实现代码里,返回了一个Binder,self.base就是viewController本身,后面的闭包提供两个参数:视图控制器(vc)和isAnimating值(active)。如果active为true,我们就用
vc.startAnimating()
显示正在加载动画,如果active为false,就用vc.stopAnimating()
隐藏加载动画。
现在我们的loading已经准备好了接收来自ViewModel的数据。让我们来看看其他的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)
在上面的代码中,每当有来自ViewModel的错误出现时,我们都会订阅到它。我们可以对监听到的错误进行进一步处理,比如显示一个弹出窗口什么的。
那么.observeOn(MainScheduler.instance)又是什么呢?它的作用是回到主线程发送错误信号。
绑定Albums 和 Tracks
现在我们将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)
数据请求
现在我们再回到ViewModel中,编写数据请求部分:
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"))
}
}
})
}
- 首先我们对loading发送一个true值,因为我们已经在HomeVC类中绑定了loading,这样我们的viewController就会显示正在加载动画。
- 接下来,发送一个数据请求,得到响应后,在回调闭包里我们应该结束加载动画,所以又发送了一个false值。
- 对于请求结果result我们做了区分,如果为success,就解析数据并发出albums和albums的值,如果为false,就会发出错误值,根据错误不同的情况我们再做相应的处理。
现在我们的数据准备好了,我们传递给了我们的子视图控制器,最后我们应该在CollectionView和TableView中显示数据了。
TracksTableViewVC中:
public var tracks = PublishSubject<[Track]>()
AlbumsCollectionViewVC中:
public var albums = PublishSubject<[Album]>()
现在在trackTableViewVC的ViewDidLoad
方法里,将数据源tracks绑定给UITableView:
tracks.bind(to: tracksTableView.rx.items(cellIdentifier: "TracksTableViewCell", cellType: TracksTableViewCell.self)) { (row,track,cell) in
cell.cellTrack = track
}.disposed(by: disposeBag)
仅仅两行代码就搞定了,不需要想以前那样设置delegate和datasource,也不用写一大堆代理方法,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 添加动画:
// 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)
最后完成效果:
小结
我们使用RxSwift和MVVM实现了一个小Demo,其中只是涉及到部分概念,后续会通过实践来学习其他的用法。