- 原文地址:Practical MVVM + RxSwift
- 原文做者:Mohammad Zakizadeh
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:iWeslie
- 校对者:swants
今天咱们将使用 RxSwift 实现 MVVM 设计模式。对于那些刚接触 RxSwift 的人,我 在这里 专门作了一个部分来介绍。前端
若是你认为 RxSwift 很难或使人十分困惑,请不要担忧。它一开始看上去彷佛很难,但经过实例和实践,就会将变得简单易懂👍。android
在使用 RxSwift 实现 MVVM 设计模式时,咱们将在实际项目中检验此方案的全部优势。咱们将开发一个简单的应用程序,在 UICollectionView 和 UITableView 中显示林肯公园(RIP Chester🙏)的专辑和歌曲列表。让咱们开始吧!ios
App 主页面git
我但愿在构建咱们的 app 时遵循可重用性原则。所以,咱们将会稍后在 app 的其余部分中重用这些 view,从而来实现咱们的专辑的 CollectionView 和歌曲的 TableView。例如,假设咱们想要显示每张专辑中的歌曲,或者咱们有一个部分用来显示类似的专辑。若是咱们不但愿每次都重写这些部分,那最好去重用它们。github
那咱们该怎么作呢? 你正好能够尝试一会儿控制器。 为此,咱们使用 ContainerView 将 UIViewController 分为两部分:swift
如今父控制器包含两个子控制器(要了解子控制器,你能够阅读 这篇文章)。后端
如今咱们的 main ViewController 就变成了:设计模式
咱们为 cell 使用 nib,这样很容易就能够重用它们。数组
要注册 nib 的 cell,你应该将此代码放在 AlbumCollectionViewVC 类的 viewDidLoad 方法中。这样 UICollectionView 才能知道它正在使用 cell 的类型:服务器
// 为 UICollectionView 注册 'AlbumsCollectionViewCell'
albumsCollectionView.register(UINib(nibName: "AlbumsCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: String(describing: AlbumsCollectionViewCell.self))
复制代码
请看在 AlbumCollectionViewVC 中的这些代码。这意味着父类对象暂时没必要处理其子类。
对于 TrackTableViewVC,咱们执行相同的操做,不一样之处在于它只是一个 tableView。如今咱们要去父类里设置咱们的两个子类。
正如你在 storyboard 中看到的那样,子类所在的地方的是放置了两个 viewController 的 view。这些 view 称为 ContainerView。咱们可使用如下代码设置它们:
@IBOutlet weak var albumsVCView: UIView!
private lazy var albumsViewController: AlbumsCollectionViewVC = {
// 加载 Storyboard
let storyboard = UIStoryboard(name: "Home", bundle: Bundle.main)
// 实例化 View Controller
var viewController = storyboard.instantiateViewController(withIdentifier: "AlbumsCollectionViewVC") as! AlbumsCollectionViewVC
// 把 View Controller 做为子控添加
self.add(asChildViewController: viewController, to: albumsVCView)
return viewController
}()
复制代码
如今咱们的 view 已经准备好了,咱们接下来须要 ViewModel 和 RxSwift:
在 HomeViewModel 类中,咱们应该从服务器获取数据,并为 view 须要展现的东西进行解析。而后 ViewModel 将它提供给父类,父控制器将这些数据传递给子控制器。这意味着父类从其 ViewModel 请求数据,而且 ViewModel 先发送网络请求,再解析数据并传给父类。
下图可让你更好地理解:
GitHub 中有个在 RxSwift 不包含 Rx 已完成的项目。在 MVVMWithoutRx 分之上没有实现 Rx。在本文中,咱们将介绍 RxSwift 的方案。请看不包含 Rx 的部分,那是经过闭包实现的。
如今是激动人心的添加 RxSwift 部分🚶♂️。在这以前,让咱们了解一下 ViewModel 应该为咱们的类提供什么:
所以父类有三种须要注册的 Observable。
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()
复制代码
这些是咱们的 ViewModel 类的成员变量。全部这四个都是没有默认值的 Observable。如今你可能会问什么是 PublishSubject 呢?
正如咱们以前在 这篇文章 里说起的,有些变量是 Observer,有些变量是 Observable。还有一种变量既是 Observer 又是 Observable,这种变量被称为 Subject。
Subject 自己分为 4 个部分(若是单独解释每一个部分,那可能须要另外一篇文章)。但我在这个项目中使用了 PublishSubject,这是最受欢迎的一个项目。若是你想了解更多关于 Subject 的信息,我建议你阅读 这篇文章。
使用 PublishSubject 的一个很好的理由是你能够在没有初始值的状况下进行初始化。
如今让咱们看看具体代码,如何才能将数据提供给咱们的 view:
在咱们看 ViewModel 的代码以前,咱们须要让 HomeVC 监听 ViewModel 并在其改变时更新 view:
homeViewModel.loading.bind(to: self.rx.isAnimating).disposed(by: disposeBag)
复制代码
在这段代码中,咱们将 loading
绑定到 isAnimating
,这意味着每当 ViewModel 改变 loading
的值时,咱们 ViewController 的 isAnimating
值也会改变。你可能会问是否仅使用该代码显示加载动画。答案是确定的,但须要一些延迟,我稍后会解释。
为了把咱们的数据绑定到 UIKit,这有利于 RxCocoa,能够从不一样的 View 中得到不少属性,你能够经过 rx
访问这些属性。这些属性是 Binder,所以你能够轻松地进行绑定。那这又是什么意思呢?
这意味着每当咱们将 Observable 绑定到 Binder 时,Binder 就会对 Observable 的值做出反应。例如,假设你有一个 Bool 的 PublishSubject,它只有 true 和 false。若是将此 subject 绑定到 view 的 isHidden 属性,则在 publishSubject 为 true 时将隐藏 view。若是 publishSubject 为 false,则 view 的 isHidden 属性将变为 false,而后将再也不隐藏 view。这是否是很酷?
多亏了 Rx 团队的 RxCocoa 包含了许多 UIKit 的属性,可是有些属性(例如自定义属性,在咱们的例子中是 Animating)是不在 RxCocoa 中的,但你能够轻松添加它们:
extension Reactive where Base: UIViewController {
/// 用于 `startAnimating()` 和 `stopAnimating()` 方法的 binder
public var isAnimating: Binder<Bool> {
return Binder(self.base, binding: { (vc, active) in
if active {
vc.startAnimating()
} else {
vc.stopAnimating()
}
})
}
}
复制代码
如今让咱们解释一下上面的代码:
Binder<Bool>
的 UIViewController,以即可以绑定。vc
)和 isAnimating (active
)传值。因此咱们能够在 isAnimating
的每一个值中说明 viewController 会发生什么变化,因此若是 active
为 true,咱们用 vc.startAnimating()
显示加载动画,并在 active
为 false 时隐藏。如今咱们的加载已准备好从 ViewModel 接收数据了。那么让咱们看看其余的 Binder:
// 监听显示 error
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 每产生一个 error 时,咱们都会监听到它。你能够用 error 作任何你想作的事情(我正在弹出一个窗口)。
什么是 .observeOn(MainScheduler.instance)
呢?🤔这部分代码将发出的信号(在咱们的例子中是 error)带到主线程,由于咱们的 ViewModel 正在从后台线程发送值。所以咱们能够防止因为后台线程而致使的运行时崩溃。你只需将信号带到主线程中,而不是执行 DispatchQueue.main.async {}
。
如今让咱们为 UICollectionView 和 UITableView 的专辑和曲目进行绑定。由于咱们的 tableView 和 collectionView 属性在咱们的子控中。如今,咱们只是将 ViewModel 中的专辑和曲目数组绑定到子控的曲目和专辑属性,并让子控负责显示它们(我将在文章末尾展现它是如何完成的):
// 把专辑绑定到 album 容器
homeViewModel
.albums
.observeOn(MainScheduler.instance)
.bind(to: albumsViewController.albums)
.disposed(by: disposeBag)
// 把曲目绑定到 track 容器
homeViewModel
.tracks
.observeOn(MainScheduler.instance)
.bind(to: tracksViewController.tracks)
.disposed(by: disposeBag)
复制代码
如今让咱们回到 ViewModel 看看发生了什么:
public func requestData(){
// 1
self.loading.onNext(true)
// 2
APIManager.requestData(url: requestUrl, method: .get, parameters: nil, completion: { (result) in
// 3
self.loading.onNext(false)
switch result {
// 4
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)
// 5
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 类中进行了绑定,咱们的 viewController 如今显示了加载动画。loading
发送 false 来结束加载动画。let albums = returnJson["Albums"].arrayValue.compactMap { return Album(data: try! $0.rawData()) }
let tracks = returnJson["Tracks"].arrayValue.compactMap { return Album(data: try! $0.rawData()) }
self.albums.append(albums)
self.tracks.append(tracks)
复制代码
如今咱们的数据准备好了,咱们传递给子控,最后该在 CollectionView 和 TableView 中显示数据了:
若是你还记得 HomeVC:
public var tracks = publishSubject<[Track]>()
复制代码
如今在 trackTableViewVC 的 viewDidLoad 方法中,咱们应该将曲目绑定到 UITableView,这能够只用两三行代码行中完成。感谢 RxCocoa!
tracks.bind(to: tracksTableView.rx.items(cellIdentifier: "TracksTableViewCell", cellType: TracksTableViewCell.self)) { (row,track,cell) in
cell.cellTrack = track
}.disposed(by: disposeBag)
复制代码
是的你只须要三行,事实上是一行,你不须要再设置 delegate 或 dataSource,再也不有 numberOfSections,numberOfRowsInSection 和 cellForRowAt。RxCocoa 一次性可处理全部内容。
你只须要将 Model 传递给 UITableView 并为其指定一个 cellType。在闭包中,RxCocoa 将为你提供与模型数组对应的单元格,model 和 row,以便你可使用相应的 model 为 cell 提供信息。在咱们的 cell 中,每当调用 didSet 时,cell 将使用 model 设置属性。
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
}
}
复制代码
固然,你能够在闭包内更改 view,但我更喜欢用 didSet。
在本文结束以前,让咱们经过添加一些动画给咱们的 tableView 和 collectionView 焕发活力:
// cell 的动画
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)
复制代码
咱们的项目最终会变成下面这样:
动态 demo
咱们在 RxSwift 和 RxCocoa 的帮助下在 MVVM 中实现了一个简单的 app,我但愿你对这些概念更加熟悉。若是你有任何建议能够联系咱们。
最终完成的项目能够在 GitHub 仓库 下找到。
若是你喜欢这篇文章和项目,请不要忘记,你能够经过 Twitter 或经过电子邮件 mohammad_Z74@icloud.com 联系本文做者。
感谢你的阅读!
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。