如何使用 MVP 模式

MVP 模式中,最重要的是 Model,由於 MVP 中,會廣泛的用到觀察者模式來完成彼此間的溝通,為了簡化程式碼,我們透過 LionEvents 中的 EventDispatcher 來實踐。

不管是透過 Notification 或是 KVO 或是 LionEvents都可以。

打開FlickrModel,我們再讓他繼承EventDispatcher

import LionEvents
class FlickrModel:EventDispatcher {
//...略
}

下一步,我們來好好思考我們需要這App有怎樣的互動:

  1. 資料載入完成以後顯示圖片

  2. 點選現在的照片以後可以看到大張圖片

  3. 可以切換是否高亮的顏色佈景主題

  4. 可以翻頁,翻頁後要換圖片

所以我們會需要 FlickrModel 能夠廣播/通知上述這四種不同的時機。

    static let FLICKR_DATA_COMPLETE :String = "flickrDataComplete"
    static let PHOTO_SELECT_CHANGE  :String = "photoSelectChange"
    static let THEME_STYLE_CHANGE   :String = "themeStyleChange"
    static let PAGE_INDEX_CHANGE    :String = "pageIndexChange"

我們先前在 FlickrModel設計好的變數在這邊就會派上用場了, 在 OOP 的主要思想下,封裝與權限控制是基本,所以 FlickrModel 裡面我們原本設定了不少變數,但是都是私有的,只有在有必要時才會開啟。

所以我們在去想其他類別會去讀取的變數與資料有什麼?再分別依序開啟:

    var perPage:UInt{
        return mPerPage
    }

    var currentPageIndex:UInt{
        set{
            mCurrentPageIndex = newValue
            let _event:Event = Event(aType: FlickrModel.PAGE_INDEX_CHANGE)
            dispatchEvent(_event)
            self.loadFlickrData(mCurrentPageIndex)
        }
        get{
            return mCurrentPageIndex
        }
    }

    var isNight:Bool{
        get{
            return mIsNight
        }
        set{
            mIsNight = newValue
            let _event:Event = Event(aType: FlickrModel.THEME_STYLE_CHANGE)
            dispatchEvent(_event)
        }
    }

我們會透過 FlickrModel 來記錄儲存現在這隻 App 所要呈現的狀態,包含了

  1. 每一頁要顯示幾張圖

  2. 是否要換成夜晚的佈景主題

  3. 目前顯示的是第幾頁

而在切換佈景主題以及被更改目前是第幾頁的時候,會再廣播相對應的事件FlickrModel.PAGE_INDEX_CHANGEFlickrModel.THEME_STYLE_CHANGE

而 PhotoModel裡原本的FLICKR_DATA_COMPLETE通知,除了保留原有功能以外,我們再追加事件的廣播。

    private func postFlickrDataCompleteNotification(aPageIndex:UInt){
        let _userObject:[NSObject:AnyObject] = ["pageIndex":aPageIndex]
        NSNotificationCenter.defaultCenter().postNotificationName(FlickrModel.FLICKR_DATA_COMPLETE, object: self, userInfo:_userObject )

        let _event:Event = Event(aType: FlickrModel.FLICKR_DATA_COMPLETE)
        _event.information = aPageIndex
        self.dispatchEvent(_event)
    }

然後,我們再建立一個Presenter的分組,放一個 Presenter類別,MVP 模式下的 P ,指的是Presenter,他主要功能是用做視覺互動:

先建立一個 Presenter 的類別,而且不繼承任何類別,在宣告一個 weak 弱引用的 FlickrModel,在建構式時,就連同 view 的屬性一起設好,因為沒有 view,也沒有Presenter存在的必要了。

然後再對 model 設定 set 與 get,只要 model 被設定有值之後,就立刻註冊偵聽需要用到的四個事件,並且先寫好空的事件函數。

import Foundation
import UIKit
import LionEvents

class Presenter {
    private weak var mModel:FlickrModel?
    var model:FlickrModel? {
        get{
            return mModel
        }
        set{
            mModel = newValue
            mModel?.addEventListener(FlickrModel.FLICKR_DATA_COMPLETE, onFlickrDataCompleteHandler)
            mModel?.addEventListener(FlickrModel.PHOTO_SELECT_CHANGE, onPhotoSelectChangeHandler)
            mModel?.addEventListener(FlickrModel.THEME_STYLE_CHANGE, onThemeStyleChangeHandler)
            mModel?.addEventListener(FlickrModel.PAGE_INDEX_CHANGE, onPageIndexChangeHandler)
        }
    }

    var view:UIView

    init(aView:UIView){
        self.view = aView
    }

    deinit{
        mModel?.removeEventListener(FlickrModel.FLICKR_DATA_COMPLETE)
        mModel?.removeEventListener(FlickrModel.PHOTO_SELECT_CHANGE)
        mModel?.removeEventListener(FlickrModel.THEME_STYLE_CHANGE)
        mModel?.removeEventListener(FlickrModel.PAGE_INDEX_CHANGE)
    }

    func onFlickrDataCompleteHandler(aEvent:Event){

    }

    func onPageIndexChangeHandler(aEvent:Event){

    }

    func onThemeStyleChangeHandler(aEvent:Event){

    }

    func onPhotoSelectChangeHandler (aEvent: Event) {

    }
}

上述四個事件函數,為了要讓其他繼承的類別可以複寫,所以權限記得至少要開到 internal。

這樣,我們就可以針對於 PhotoView 專門寫一個 PhotoPresenter ,繼承Presenter類別,讓他完成功能事件內的事情:

import Foundation
import LionEvents

class PhotoPresenter: Presenter {

    override func onThemeStyleChangeHandler(aEvent: Event) {
        if let _photoView:PhotoView = self.view as? PhotoView{
            _photoView.setHighlight(self.model!.isNight)
        }
    }
}

然後我們再修改一下 MainViewController.swiftviewDidLoad 的部分,並建立一個新的函數以方便整理:

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.whiteColor()

        //createDemoLayout()
        createDemoLayoutSecond()
        createMVPDemo()
    }

    private func createMVPDemo(){

        for _photoView:PhotoView in mPhotoViews {
            let _photoPresenter:PhotoPresenter = PhotoPresenter(aView:_photoView)
            _photoPresenter.model = mFlickrModel
        }

    }

追加一個 createMVPDemo 的函數,然後把PhotoPresenter都建立起來,不要忘記給PhotoPresenter設定指定的 model。

再修改一下原本的onPhotoViewTouchHandler

    private func onPhotoViewTouchHandler(aEvent:Event){
        //print("\(aEvent.type),target:\(aEvent.target),currentTarget:\(aEvent.currentTarget)")
        if let _:PhotoView = aEvent.target as? PhotoView {
            //_photoView.setHighlight(true)
            mFlickrModel.isNight = !mFlickrModel.isNight
        }
    }

現在每次點擊,我們並不是直接去更改 PhotoView 是否要高光顯示,而是透過 FlickrModel裡的isNight屬性的更改,讓PhotoPresenter知道所屬的PhotoView的需要被改變。

大家應該有發現到,原本的 MainViewController.swift 似乎變得十分雜亂。

其實 cocoa的 MVC 模式,UIViewController不只是單純的MVC下的Controller而已,還包含了Dynamic Layout的功能,尤其我們的畫面全部都是使用Hard Coding產生的時候,我們並沒有特別把MainViewController裡面會用到的 View 另外寫成一個類別,所以現在的MainViewController不只是只有單純的 Controller 功能,還包含了視覺排版Dynamic Layout,我們都寫進了這個的類別裡面了。

為了讓 MVP 這個功能獨立化,我們再建立一個專屬於這個應用程式所用的 Controller,我們新建一個Controller.swift,不過這個Controller我們要的是完全乾淨的只有Controller功能的,所以也不需要去繼承任何類別。

import Foundation
import LionEvents

class Controller {
    weak var model:FlickrModel?

    init(aModel:FlickrModel){
        model = aModel
    }
}

然後我們再幫Controller建立他的新功能:讓只要是繼承自UIView的類別,都可以當該功能的控制器:

    func addStyleChangeController(aView:UIView){
        let _swipeGesture:UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: Selector("onStyleSwipeHndler:"))
        _swipeGesture.direction = [ UISwipeGestureRecognizerDirection.Left, UISwipeGestureRecognizerDirection.Right]
        aView.addGestureRecognizer(_swipeGesture)
    }

    @objc private func onStyleSwipeHndler(aSender:UISwipeGestureRecognizer) {
        self.model!.isNight = !self.model!.isNight
    }

再回到MainViewController裡面,先再幫Controller設定一個全域變數:

private var mController:Controller?

再找到createMVPDemo並且追加下面程式:

    private func createMVPDemo(){

        for _photoView:PhotoView in mPhotoViews {
            let _photoPresenter:PhotoPresenter = PhotoPresenter(aView:_photoView)
            _photoPresenter.model = mFlickrModel
        }

        mController = Controller(aModel: mFlickrModel)
        mController!.addStyleChangeController(self.view)
    }

在模擬器上執行測試,你們會發現左右滑動,也可以直接更改整個畫面的佈景風格了!

花盡了心思,把這個功能寫好了,目前只會覺得一個簡單的功能把它拆得很多類別,這樣到底有什麼好處呢?

下一個步驟,就是他最大的好處要展現了!!

完成到這一步的Demo:

Last updated