🦅

【Swift】MVCについて分かりやすいサンプルが無かったのでSwiftでMVCパターンで簡易アプリ書いてみた【MVCパターン】

2022/02/05に公開

表題の通りです。

MVCパターンについて解説書いている記事は沢山存在しますが、
github等でソースコードは公開していないので、
初めて勉強される方など不明確な領域かと思い、
私の方でMVCパターンで簡易なアプリを開発してみました。

ソースコードについても貼っておきます。

また、UIViewController(Controller)にUITableViewDelegateUITableViewDataSourceを書くのではなく、ModelにUITableViewDataSourceを切り分けて実装しているので、そういった内容を求めている方もMVCパターンが分からなくてもサンプルコードが短いのでご参考いただけますと幸いです。

書籍紹介

iOSアプリ設計パターン入門
こちらはかなり分かりやすく、読みやすいです。
もしよかったら3200円の価値はあるのでぜひ購入してみてください。

https://peaks.cc/books/iOS_architecture

簡易のアプリ機能

上記のように、アプリ開発でメインで使われるUITableViewを利用してMVCパターンを採用しました。

初期表示時に3件のセルを表示しています。
何かしらのセルを押下することで、都度セルが1件追加されます。
それだけの機能を有しております。

MVCパターンについて

MVCパターンにおける一般的なModel、View、Controllerについて先に触れ、
その後、本サンプルアプリにおける期待する役割を記載します。

■Model

システムの中でビジネスロジックを担当する。(上記書籍では「「UI に関係しない処理すべて」 と形容すべき、ざっくりした存在です 」とも記述されております。)

■View

ユーザーが直接目にするView部分の描画処理を行う。

■Controller

何かしらの入力に対する適切な処理を行うだけでなく、Model オブジェクトと Viewオブジェクトも保持します。Model オブジェクトに処理を依頼し、受け取った結果を使っ て View オブジェクトへ描画を指示します。
今回ではControllerがModelとViewの橋渡し役になります。
(ModelとViewは参照関係にないです。)

今回ではControllerがModelとViewの橋渡し役になります。
(ModelとViewは参照関係にないです。)

サンプルアプリでのModelの責務

  • ドメイン(ビジネスロジックを担当する)
  • TableViewのdatasourceの責務はModelにある。
  • 通知でViewControllerに変更を知らせる(配列の内容を変更する、など)
  • 変数などを処理する(表示用の配列など)

サンプルアプリでのViewの責務

  • UIViewクラスを生成し、xibファイルと紐づける(UIViewControllerでxibファイルを持たない)

サンプルアプリでのControllerの責務

  • View側の押下時の処理はControllerで設定する。
    • ボタンを押下した際の処理はaddTarger:を利用してControllerからModelに処理を橋渡しする。
  • 画面遷移の処理を挟むので、TableViewのdelegateの責務はここにある。
  • FATな処理になりそうなら、Modelにすぐに書く。

クラス構成と解説

  • TweetListModel.Swift (Model) ※class
  • TweetListView.Swift (View) ※UIView
  • TweetViewController(Controller) ※UIViewController

Model

まずデータの表示の為の処理と、
テーブルセルを押すと件数が増える処理をModelに記載しております。
また、UITableViewのUITableViewDatasourceのような表示するセルの個数を確定する処理(func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {)は、Controllerで担当するべき責務では無い為、Modelで対応します。

import Foundation
import UIKit
 
// ツイート自体のデータモデルも作る
class TweetDataModel {
    let tweet: String
    
    init(initTweetText: String) {
        tweet = initTweetText
    }
}

class TweetListModel: NSObject,UITableViewDataSource {
    
    // Modelを監視するクラス
    let notificationCenter = NotificationCenter()
    
    // Modelで管理する配列に初期値を設定する。
    private(set) var tweetList: [TweetDataModel] = [
        TweetDataModel.init(initTweetText:"Tweet: 0番目"),
        TweetDataModel.init(initTweetText:"Tweet: 1番目"),
        TweetDataModel.init(initTweetText:"Tweet: 2番目")
    ] {
        didSet{
            // Modelで管理している配列に変化があった場合に呼び出されて、通知する。
            notificationCenter.post(name: .init(rawValue: "changeTweetList"), object: nil, userInfo: ["list" : tweetList])
        }
    }
    
    // 配列に新しいツイートを追加する。
    func addTweetList() {
        let tweetText = "Tweet:\(self.tweetList.count)番目"
        self.tweetList.append(TweetDataModel.init(initTweetText: tweetText))
    }
    
    
    // MARK: UITableViewDatasoruce
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.tweetList.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
        let tweetModel = self.tweetList[indexPath.row]
        cell.textLabel?.text = tweetModel.tweet
        return cell
    }
}


View

TweetListView.swiftとTweetListView.xibを用意します。
参考画像を元にswiftファイルとxibファイルの紐付けも済ませてください。

基本的には以下のコードをコピペで大丈夫です。
func loadNib() 内のTweetListViewはxibファイル名を参考にしています。
なのでファイル名に応じて変更してください。


import UIKit

class TweetListView: UIView {
   @IBOutlet weak var tableView: UITableView!
   
   override init(frame: CGRect){
       super.init(frame: frame)
       loadNib()
   }

   required init(coder aDecoder: NSCoder) {
       super.init(coder: aDecoder)!
       loadNib()
   }

   func loadNib(){
       let view = Bundle.main.loadNibNamed("TweetListView", owner: self, options: nil)?.first as! UIView
       view.frame = self.bounds
       self.addSubview(view)
   }

}

Controller

先に記述を置いていきます。
ViewControllerでTableViewのデリゲートメソッドを宣言してます。

Controllerの役割は以下の二点です。

  • Viewでのユーザーアクションを検知して、Modelに処理を走らせる。
  • Modelの値の変化を受けて、Viewを反映する。
import UIKit

class TweetViewController: UIViewController {
    
    var myModel: TweetListModel? {
        // セットされるたびにdidSetが動作する
        didSet {
            // ViewとModelとを結合し、Modelの監視を開始する
            registerModel()
        }
    }

    

    override func loadView() {
        super.loadView()
        self.view = TweetListView()
        
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.myModel = TweetListModel()
        settingTableView()
    }
    
    private func settingTableView () {
        let tweetListView = self.view as! TweetListView
        tweetListView.tableView.delegate = self
        tweetListView.tableView.dataSource = self.myModel
        // TableViewに表示するCellを登録する
        tweetListView.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
    }
    
    private func registerModel() {
        guard let model = myModel else { return }
        
        // 配列が変化したらnotificationCenterで通知を受け取る。
        model.notificationCenter.addObserver(forName: .init(rawValue: "changeTweetList"),
                                             object: nil,
                                             queue: nil,
                                             using: {
            [unowned self] notification in
            let tweetListView = self.view as! TweetListView
            
            tweetListView.tableView.reloadData()
        })
    }
    
    // TableViewのセルのタップを検知して、Modelの配列追加する処理を呼び出す。
    @objc func onTapTableViewCell() { myModel?.addTweetList() }
}

// TableViewを描画・処理する為に最低限必要なデリゲートメソッド、データソース
extension TweetViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // Modelでタップされた時の追加処理を行う。
        self.onTapTableViewCell()
    }
}

}```

# 最後に

このように設計することで、Viewは描画の責務に専念し、
Modelはビジネスロジックに専念し、ControllerView-Modelの橋渡しに徹底しております。



Discussion