- 原文地址: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、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。