11月29日に筋肉.swiftという所謂普通のiOS勉強会を主催しまして、イベントを盛り上げるためにiOSアプリをリリースしました。

https://itunes.apple.com/jp/app/id1312864123?mt=8

今回、作るに当たってある程度テーマを設けたいなと思いまして、

Cloud Firestoreを使ってみる
MVPパターンで実装してみる

という2つのテーマに設定しました。

そのうち今回は、後者のMVPパターンで実装してみるをやってみて感じたメリット、疑問に思ったことなどを書いていきたいと思います。

ちなみにソースコードは以下にあります。

https://github.com/kboy-silvergym/KinnikuSwift

MVPパターンとは

スクリーンショット 2017-12-01 0.11.30.png
出典:https://speakerdeck.com/amacou/mvpfalseyounamofalsewotimuniti-an-sitahua?slide=23

スクリーンショット 2017-12-01 0.11.59.png
出典:https://speakerdeck.com/amacou/mvpfalseyounamofalsewotimuniti-an-sitahua?slide=29

非常にわかりやすかったので上のスライドをシェアさせていただきました。

今回、MVPを採用する目的としては、ViewControllerがでかくなるのを解消できるのか? を個人的に検証するため。サンプルじゃなくて実際に動かすことを目的としたアプリでどうなるのか検証したかったため、筋肉.swiftアプリを使ってやってみました。

以下、実際に実装して見て、メリットを感じた例、意味なくね?と思った例、これでいいのか?と疑問に思った例をあげていきたいと思います。もし知見のある方がいましたらご意見お願いしたいです。

メリットを感じた例

登壇してくれるスピーカー一覧を表示するSpekerListViewControllerを実装するに当たってPresenterはViewControllerを薄くすることに貢献してくれました。

getSpeakerRealtimesortgetVoteAlertという3つのメソッドがあり、これをこのままViewControllerに書くと60行くらいは占有します。ViewControllerがとてもでかくなって見にくくなるところをPresenterが請け負うことで回避できたと思いました。

https://github.com/kboy-silvergym/KinnikuSwift/blob/master/kinniku-swift/View/Scene/SpeakerList/SpeakerListPresenter.swift

意味なくね?と思った例

自分で、MVPにしてやる!というテーマに決めたので意味ない気がしても無理やりPresenterを作ることにしました。

FeedPresenter.swift
import UIKit

class FeedPresenter {

    func getTweet(_ completion: @escaping ([Tweet]) -> Void){
        TwitterAPI.getTweet({ results in
            guard let tweets = results?.tweets else {
                completion([])
                return
            }
            completion(tweets)
        })
    }

    func showTweetComposer(_ vc: UIViewController){
        TwitterAPI.showTweetComposer(fromVC: vc, completion: { result in
            switch result {
            case .done:
                break
            case .cancelled:
                break
            }
        })
    }
}

https://github.com/kboy-silvergym/KinnikuSwift/blob/master/kinniku-swift/View/Scene/Feed/FeedPresenter.swift

呼び出し側のViewControllerのメソッドが以下。

FeedViewController.swift
    func getTweet(){
        presenter.getTweet({ tweets in
            self.tweets = tweets
            self.refreshControl?.endRefreshing()
            self.tableView.reloadData()
        })
    }

    @IBAction func addButtonTapped(_ sender: Any) {
        presenter.showTweetComposer(self)
    }

https://github.com/kboy-silvergym/KinnikuSwift/blob/master/kinniku-swift/View/Scene/Feed/FeedViewController.swift

ViewからModelを直接触らないというルールを守るためにTwitterAPIクラスをPresenterにおくようにしてみました。しかし、これではModelを直接触らないルールが守られているだけで、ViewControllerを小さくするという目的が達成されていません。伝言ゲームをしているだけ。

これが意味なくね?と思ったポイントです。

疑問に思った例

さらに、同じ部分のFeedPresenterまわりのMVP実装で疑問に思うことがあります。

以下にFeedViewControllerを全て貼ります。

FeedViewController

import UIKit
import InteractiveSideMenu

class FeedViewController: UIViewController {
    let presenter = FeedPresenter()

    @IBOutlet weak var tableView: UITableView!
    var refreshControl: UIRefreshControl?

    var tweets: [Tweet] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.dataSource = self
        tableView.tableFooterView = UIView()

        refreshControl = UIRefreshControl()
        refreshControl?.addTarget(self, action: #selector(type(of: self).refresh), for: .valueChanged)
        tableView.refreshControl = refreshControl

        let nib = UINib(nibName: String(describing: TweetCell.self), bundle: nil)
        tableView.register(nib, forCellReuseIdentifier: String(describing: TweetCell.self))

        getTweet()
    }

    @objc func refresh(){
        getTweet()
    }

    func getTweet(){
        presenter.getTweet({ tweets in
            self.tweets = tweets
            self.refreshControl?.endRefreshing()
            self.tableView.reloadData()
        })
    }

    @IBAction func addButtonTapped(_ sender: Any) {
        presenter.showTweetComposer(self)
    }

    @IBAction func menuButtonTapped(_ sender: Any) {
        if let navigationViewController = self.navigationController as? SideMenuItemContent {
            navigationViewController.showSideMenu()
        }
    }
}

// MARK: - <#UITableViewDataSource#>
extension FeedViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tweets.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TweetCell.self), for: indexPath) as! TweetCell
        cell.tweet = tweets[indexPath.row]
        return cell
    }
}

疑問は、var tweets: [Tweet] = []はFeedViewControllerのメンバ変数にあっていいのか?という点です。ドメインモデルを管理するのはもしかしてPresenterであるべきなのではないか!?という懸念があります。

しかし、上記にある通り、FeedViewControllerUITableViewDataSourceのメソッドを持っていて、cellにtweetをセットするためにViewControllerにtweetはメンバ変数としてあって欲しいところです。それとも

cell.tweet = presenter.tweet[indexPath.row]

みたいになるべき?

なにかしっくり来ない気がします。

UITableViewDataSourceはView/ViewController側の責務としてあるべきだと思うので、UITableViewDataSourceをPresenterが持つというのもルール違反な気がしています。そもそもそしたら複雑になりそうですし。。。

疑問点以上です。。

追記

Twitterでコメントいただいた @fumiyasac@github さんの例でもtableViewの要素を示すphotoContents的なモデルは、ViewControllerのメンバ変数に置いていました。

やはりこれがtableViewにおけるMVPのベストプラクティスかもしれない。

まとめ

ということで、後半はstackOverFlowの質問みたいになってしまいましたが、ViewControllerを薄くしたい!という目的においてMVPの恩恵を感じられた部分もありました!

引き続きデザインパターンについて知見を貯めていきたいなと考えています!
知見がある方いましたらアドバイスいただきたく思います!

github以下にあります。

https://github.com/kboy-silvergym/KinnikuSwift