Link.

前情提要

Part1 和 Part2 分別講述了我對 Functional ProgrammingRxSwift 實作觀察者模式的看法

至於 MVP 是我現在專案的程式架構,本篇開始將會舉我在 MVP 架構下使用 RxSwift 的一些例子,理論上會是個長期計劃,在我忘記以前我會記得更新的
當然這些只是我個人的做法,有任何想法建議希望可以提出,我會非常感謝你的

關於 MVP 的功能架構我是參考 Google Architecture
可以花些時間看看 Google 是怎麼架構 MVP的

簡短的介紹一下 MVP 的構照,就是將程式分成

  • Repository
    Repository 就是 MVP 中的 Model,負責一切資料的存取
  • View
    畫面顯示
  • Presenter
    負責 View 對應所需的一切邏輯操作

ViewPresenter 是一對一的關係,我分類是依功能區分
ViewPresenter 要做的事都會定義在 Protocol,可以一目瞭然知道這個功能在做什麼
ViewviewDidLoad 要初始化一個 Presenter,並且把自己的 viewProtocol 傳進去,所有 View 要做什麼都由 Presenter 發號司


以下舉公告列表這個功能為例,編輯跟搜尋先不管
只處理從伺服器拿到公告消息資料並顯示在 Table 上這個任務

Announcement 資料夾裡就會有

  • AnnouncementProtocol
  • AnnouncementPresenter
  • AnnouncementViewController

Repository 資料來裡有

  • AnnouncementRepository

先看 AnnouncementProtocol 的內容,分別列出 PresenterView 的任務
以目前功能來說,View 只有一個動作,點選公告列表
Presenter 也只需要做一件事,將 tabledata 綁定

protocol AnnouncementViewProtocol {
    func announcementSelected(model: AnnouncementModel)
}
protocol AnnouncementPresenterProtocol {
    func bindAnnouncementListData(tableView: UITableView)
}

接著看 Repository 的程式
AnnouncementRepositorysingleton,裡頭將公告資料型態是 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 ProtocolannouncementSelected(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,主要是簡化繁鎖的 UITableViewDelegateUITableViewDataSource

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 的程式碼非常精簡
除了本來應該出現的 UITableViewDelegateUITableViewDataSourceRxSwift 處理

View 需要的公告資料也由 PresenterRepository 索取,View 儘管理與使用者的互動關係

View 的所有動作都交由 Presenter 處理,目前功能只有一個綁定 table 與 公告的關係

Presenter 處理拿到公告資料後要做什麼事,如果需要更新資料就叫 Repository 做,需要 View 更新畫面的,就由 viewDelegate 來做,所以一切 View 的後續動作,都要宣告在 ViewProtocol

Repository 需要處理好一切資料的操作,例如 API 錯誤時回傳預設資料,資料篩選等等,確保 Presenter 只會拿到”需要”的資料


現在來做個練習題,假設下一個版本需要增加一個功能:

拿到的資料超過 30 天前要額外標註過期的狀態,我們的修改想法會是什麼?

首先在加一個 NSDateExtension 來判斷公告是否過期,得到一個 expired 布林值

那這個 expired 要存在哪裡? 原本的 AnnouncementModel 多一個參數嗎?

我比較不頃向於更動 AnnouncementModel,原因是日後可能會讓維護者誤以為 expired 是由 Server 回傳的數值,因此我會選將型態改成 tuple 多一個變數

那轉換 isExpired 的代碼應該擺在哪裡?

答案是放在 Repository,這絕對比放在 MantleTransformer 更好理解跟維護

照這個思路,首先 RepositoryannouncementList 型態將會變成 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 的型態改了,所以 ViewPresenter 也要做修改對嗎?

完全不用,因為 RxSwift 使用了泛型(Generic)來做處理

    .bindTo(tableView.rx_itemsWithCellIdentifier("cell", cellType: AnnouncementCell.self)) { (row, element, cell) in
        cell.configureCell(element)
    }

這裡拿到的 element 就會自動變成是 (AnnouncementModel, expired: Bool)
所以我們接下來唯一要修改的只有 UI 的部份,也就是 cellconfigureCell

然後 ViewannouncementSelected(),如果下一個 detail 頁面需要知道是否過期,那資料型態也得跟著修改,如果不需要知道,那就維持原狀,修改傳遞參數 announcementSelected(model.0) 把 tuple 第一個參數傳過去就好,完全不影響後續操作,也不會更動到原先的 Model


參考連結

Google Android Architecture [here]
淺入淺出 RxSwift & MVP 系列 Part 1 [here]
淺入淺出 RxSwift & MVP 系列 Part 2 [here]

CATEGORIES