Part1 和 Part2 分別講述了我對 Functional Programming、RxSwift 實作觀察者模式的看法
至於 MVP 是我現在專案的程式架構,本篇開始將會舉我在 MVP 架構下使用 RxSwift 的一些例子,理論上會是個長期計劃,在我忘記以前我會記得更新的
當然這些只是我個人的做法,有任何想法建議希望可以提出,我會非常感謝你的
關於 MVP 的功能架構我是參考 Google Architecture
可以花些時間看看 Google 是怎麼架構 MVP的
簡短的介紹一下 MVP 的構照,就是將程式分成
- Repository
Repository 就是 MVP 中的 Model,負責一切資料的存取- View
畫面顯示- Presenter
負責 View 對應所需的一切邏輯操作
View 跟 Presenter 是一對一的關係,我分類是依功能區分
View 跟 Presenter 要做的事都會定義在 Protocol,可以一目瞭然知道這個功能在做什麼
View 的 viewDidLoad 要初始化一個 Presenter,並且把自己的 viewProtocol 傳進去,所有 View 要做什麼都由 Presenter 發號司
以下舉公告列表這個功能為例,編輯跟搜尋先不管
只處理從伺服器拿到公告消息資料並顯示在 Table 上這個任務
Announcement 資料夾裡就會有
- AnnouncementProtocol
- AnnouncementPresenter
- AnnouncementViewController
Repository 資料來裡有
- AnnouncementRepository
先看 AnnouncementProtocol 的內容,分別列出 Presenter 跟 View 的任務
以目前功能來說,View 只有一個動作,點選公告列表
Presenter 也只需要做一件事,將 table 和 data 綁定
protocol AnnouncementViewProtocol {
func announcementSelected(model: AnnouncementModel)
}
protocol AnnouncementPresenterProtocol {
func bindAnnouncementListData(tableView: UITableView)
}
接著看 Repository 的程式
AnnouncementRepository 為 singleton,裡頭將公告資料型態是 Variable,這是一個 BehaviorSubject 的封裝,會在資料變更時發送 onNext()
updateAnnuncementListValue 在第一次建立 announcementList 時會運行,目的是非同步更新公告資料並將資料賦值給 announcementList
class AnnouncementRepository: NSObject {
static let sharedInstance = AnnouncementRepository()
let disposeBag = DisposeBag()
lazy var announcementList: Variable<[AnnouncementModel]> = {
AnnouncementRepository.sharedInstance.updateAnnuncementListValue()
return Variable([AnnouncementModel]())
}()
func updateAnnuncementListValue() {
APIManager.sharedInstance
.queryAnnouncements()
.map { (response) -> [AnnouncementModel] in
//用 Mantle 將 json 轉成 Model
return MantleManager.mantleArray(response)
}
.subscribeNext({ (result: [AnnouncementModel]) in
self.announcementList.value = results
})
.addDisposableTo(disposeBag)
}
}
APIManager.sharedInstance.queryAnnouncements() 是 Alamofire 從伺服器拿公告資料,回傳一個 Observable,這個 Observable 會在收到資料時 onNext
附上Alamofire API 回傳 Observable 程式參考
func queryAnnouncements() -> Observable<AnyObject> {
return Observable.create({ (observer) -> Disposable in
Alamofire.request(.POST, url_queryAnnouncements, parameters: nil)
.validate()
.responseJSON(completionHandler: { (response) in
switch response.result {
case .Success:
observer.onNext(response.result.value!)
observer.onCompleted()
break
case .Failure(let error):
observer.onError(error)
break
}
})
return NopDisposable.instance
})
}
View 的部份非常精簡,以目前功能除了本身的 override,只需實作 View Protocol 的 announcementSelected(model: AnnouncementModel)
class AnnouncementViewController: AnnouncementViewProtocol {
@IBOutlet weak var tableView: UITableView!
var presenter: AnnouncementPresenter!
override func viewDidLoad() {
super.viewDidLoad()
presenter = AnnouncementPresenter(delegate: self)
presenter.bindAnnouncementListData(tableView)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let vc = segue.destinationViewController as? DetailAnnouncementViewController, let model = sender as? AnnouncementModel {
vc.new = model
}
}
// MARK: - View Protocol
func announcementSelected(model: AnnouncementModel) {
self.performSegueWithIdentifier("detailView", sender: model)
}
}
最後是 Presenter 的程式
全部都是 RxTable 的實現,詳細程式都在 RxCocoa,主要是簡化繁鎖的 UITableViewDelegate 和 UITableViewDataSource
class AnnouncementPresenter: AnnouncementPresenterProtocol {
var tableDelegate = AnouncementTableDelegate()
var viewDelegate: AnnouncementViewProtocol!
convenience init(delegate: AnnouncementViewProtocol) {
self.init()
viewDelegate = delegate
}
func bindAnnouncementListData(tableView: UITableView) {
AnnouncementsRepository.sharedInstance
.announcementList
.asObservable()
.bindTo(tableView.rx_itemsWithCellIdentifier("cell", cellType: AnnouncementCell.self)) { (row, element, cell) in
cell.configureCell(element)
}
.addDisposableTo(disposeBag)
tableView
.rx_modelSelected(AnnouncementModel)
.subscribeNext { (model) in
self.viewDelegate.announcementSelected(model)
}
.addDisposableTo(disposeBag)
tableView.rx_setDelegate(tableDelegate)
}
}
class AnouncementTableDelegate: NSObject, UITableViewDelegate {
func tableView(tableView: UITableView, editingStyleForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCellEditingStyle {
return UITableViewCellEditingStyle.None
}
}
MVP 搭配 RxSwift 最明顯的差異是 View 的程式碼非常精簡
除了本來應該出現的 UITableViewDelegate 跟 UITableViewDataSource 由 RxSwift 處理
View 需要的公告資料也由 Presenter 跟 Repository 索取,View 儘管理與使用者的互動關係
View 的所有動作都交由 Presenter 處理,目前功能只有一個綁定 table 與 公告的關係
Presenter 處理拿到公告資料後要做什麼事,如果需要更新資料就叫 Repository 做,需要 View 更新畫面的,就由 viewDelegate 來做,所以一切 View 的後續動作,都要宣告在 ViewProtocol 中
Repository 需要處理好一切資料的操作,例如 API 錯誤時回傳預設資料,資料篩選等等,確保 Presenter 只會拿到”需要”的資料
現在來做個練習題,假設下一個版本需要增加一個功能:
拿到的資料超過 30 天前要額外標註過期的狀態,我們的修改想法會是什麼?
首先在加一個 NSDate 的 Extension 來判斷公告是否過期,得到一個 expired 布林值
那這個 expired 要存在哪裡? 原本的 AnnouncementModel 多一個參數嗎?
我比較不頃向於更動 AnnouncementModel,原因是日後可能會讓維護者誤以為 expired 是由 Server 回傳的數值,因此我會選將型態改成 tuple 多一個變數
那轉換 isExpired 的代碼應該擺在哪裡?
答案是放在 Repository,這絕對比放在 MantleTransformer 更好理解跟維護
照這個思路,首先 Repository 的 announcementList 型態將會變成 Variable<[(AnnouncementModel, expired: Bool)]>,所以 map function 要修改成
.map({ (response) -> [(AnnouncementModel, expired: Bool)] in
if let dic = response["Announcements"] as? [[NSObject : AnyObject]] {
let modelArray: [AnnouncementModel] = MantleManager.mantleArray(dic)
return modelArray.map({ (model: AnnouncementModel) -> (AnnouncementModel, Bool) in
return (model, expired: model.createdAt.isExprired())
})
}
return [(AnnouncementModel, expired: Bool)]()
})
因為 Variable 的型態改了,所以 View 跟 Presenter 也要做修改對嗎?
完全不用,因為 RxSwift 使用了泛型(Generic)來做處理
.bindTo(tableView.rx_itemsWithCellIdentifier("cell", cellType: AnnouncementCell.self)) { (row, element, cell) in
cell.configureCell(element)
}
這裡拿到的 element 就會自動變成是 (AnnouncementModel, expired: Bool)
所以我們接下來唯一要修改的只有 UI 的部份,也就是 cell 的 configureCell
然後 View 的 announcementSelected()
,如果下一個 detail 頁面需要知道是否過期,那資料型態也得跟著修改,如果不需要知道,那就維持原狀,修改傳遞參數 announcementSelected(model.0)
把 tuple 第一個參數傳過去就好,完全不影響後續操作,也不會更動到原先的 Model
參考連結
Google Android Architecture [here]