【Swift】MVPアーキテクチャの簡単なサンプル【iOS】

今回はModel-View-Presenter[MVP]アーキテクチャのデザインパターンで簡単なサンプルを作成します。

ModelではDBやAPIアクセスの処理、Presenterではその他の処理、描画はViewで行う考え方に基づき実装しました。

これから作るサンプルではModel内でRealmSwiftを使用します。

環境

・MacOS Ventura 13.0
・Xcode 14.2
・Swift version 5.7.2

1. View


UI関連の処理はここに記述します。

iOSの場合はViewControllerのクラス内に画面レイアウト関連の処理を行う形で良いと思います。

UIの配置やボタンクリックイベントの定義などはViewControllerに書く形で実装します。

2. Presenter


Viewから指定された処理を行います。

DB、APIを使用しない処理の場合はPresenter内で処理を書き、使用する場合はModelに処理を委譲する形で実装しました。

Presenter内で処理し、必要に応じて結果をViewに反映させます。

3. Model

DB,API関連の処理と処理結果を返したり、データを保持する部分をModelに記述します。

4. 完成イメージと処理の流れ

TextFieldにタイトルを入力→保存ボタンをタップしてDBにデータを保存→タイトル取得ボタンタップして保存したデータを取得→上部のLabelにタイトル名を反映させるシンプルなアプリです。


5. 実装

ここからはプロジェクトを作成し簡単なMVPのサンプルプロジェクトを作ってみます。

5-1. ファイル構成

プロジェクトのファイル構成は以下の様になりました。

Presenterフォルダには2種類のファイルを作成しています。

使い方については後述します。

【View】

・SampleViewController

【Presenter】

・SampleViewPresenter
・SampleViewOutput

【Model】

・SampleModel
・LabelInfo


5-2. RealmSwiftの導入

例)
cd /Users/ユーザー名/Documents/Projects/SampleMVP/SampleMVP.xcodeproj 

〇〇.xcodeprojを削除してプロジェクトのフォルダ直下に移動します。

cd /Users/ユーザー名/Documents/Projects/SampleMVP

プロジェクトフォルダに移動したら、プロジェクトフォルダ内にpodfileを作成

pod init

podfileを開く

open podfile

pod file内に[RealmSwift]を追加。

pod 'RealmSwift'

ライブラリのインストール

pod install

ライブラリの導入が完了したらプロジェクトを開き直します。

青のプロジェクトファイル(SampleMVP.xcodeproj)ではなく、白い方(SampleMVP.xcworkspace)から開いてください。


5-3. Modelの実装

ModelフォルダにはSampleModelとLabelInfoのファイルを作りました。

データの保存や取得の処理はSampleModelに実装します。

Realmではデータベースのテーブルを定義するクラスの作成が必要なためLabelInfoというクラスを作成してテーブルの定義をしました。

SampleModel内では文字列データの書き込み(更新)、データの取得を行う処理を作成しました。

今回はMVPのサンプルを作るのが目的のため、簡単に書きました。エラー処理については正しく書き直す必要があると思います。

SampleModel.swif

import RealmSwift
class SampleModel: NSObject {
    
    //書き込み(更新)
    func writeLabelInfo(labelTitle: String) -> Bool {
        
        let realm = try! Realm()
        let count = realm.objects(LabelInfo.self).count
        if count == 0 {
            //初回
            let labelInfo = LabelInfo()
            labelInfo.labelTitle = labelTitle
            try! realm.write {
                realm.add(labelInfo)
            }
        } else {
            //2回目以降は更新処理
            let info = realm.objects(LabelInfo.self).first
            try! realm.write {
                info?.labelTitle = labelTitle
            }
        }
        return true
    }
    
    //読み込み
    func loadLabelInfo() -> String {
        var title = ""
        let labelInfo = LabelInfo()
        let realm = try! Realm()
        let results = realm.objects(LabelInfo.self)
        if results.count != 0 {
            title = results[0].labelTitle
        } else {
            return ""
        }
        
        return title
    }
}

LabelInfoクラスではテーブル名とカラム名を定義しています。

LabelInfo.swift

//テーブル名LabelInfo カラム名labelTitle
class LabelInfo: Object {
    
    @objc dynamic var labelTitle: String = ""
    
    convenience init(labelTitle: String) {
        self.init()
        self.labelTitle = labelTitle
    }
}

5-4. Presenterの実装

PresenterフォルダにはSampleViewPresenterとSampleViewOutputの2つのフォルダを作成しました。

SampleViewOutputのprotocolにはViewにタイトルを出力するためのメソッドを定義しています。

SampleViewOutput.swift

protocol SampleViewOutput: AnyObject {
    func titleOutput(title: String)
}

SampleViewPresenterファイル内には以下のようにprotocolとclassを追記します。

saveTitleには保存ボタンを押したタップ後に取得したTextFieldの文字列がセットされます。

DBに文字列を保存したいのでModelの書き込み処理をに文字列をセットしています。

loadTitleではすでに保存されているタイトル用文字列の取得を行い、取得したタイトル文字列をViewに出力しています。データの取得はDBの処理のため、Modelに処理を任せています。

取得したタイトルはSampleViewOutputのtitleOutputにセットしてViewに出力します。

SampleViewPresenter.swift

protocol SampleViewPresenter: AnyObject {
    init(viewOutput: SampleViewOutput)
    func saveTitle(title: String)
    func loadTitle()
}
class SamplePresenter: SampleViewPresenter {
    
    weak var viewOutput: SampleViewOutput?
    private var model: SampleModel?
    
    required init(viewOutput: SampleViewOutput) {
        self.viewOutput = viewOutput
        self.model = SampleModel()
    }
    //タイトルの保存
    func saveTitle(title: String) {
        ///タイトルの保存
        guard self.model?.writeLabelInfo(labelTitle: title) == true else {
                 return
        }
    }
    //タイトルの取得
    func loadTitle() {
        //保存したタイトルの取得
        guard let title = self.model?.loadLabelInfo() else { return }
        //取得したタイトルをViewControllerに渡す
        self.viewOutput?.titleOutput(title: title)
    }
    
}

5-5. Viewの実装

ViewではUI関連の操作を行います。

各種UIのOutletやAction、presenterとの連携設定を行います。

saveActionは保存ボタンを押したときの処理です。保存ボタンを押すと、TextFieldの入力値を取得してPresenterのsaveTitleに入力値を渡します。

getTitleActionはタイトル取得ボタンを押したときの処理です。タイトル取得ボタンを押すと、PresenterのloadTitleを実行します。

loadTitleの処理内容はPresenterの実装の部分で処理内容を記載しています。

titleOutputではPresenterから出力されたタイトル名を取得してタイトル表示用ラベルに文字列をセットしています。

class SampleViewController: UIViewController {
    
    private var presenter: SamplePresenter?
    private var titleInfo: LabelInfo?
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var titleTextField: UITextField!
    @IBAction func saveAction(_ sender: Any) {
        
        let text = titleTextField.text ?? ""
        //Presenterに処理を渡す
        self.presenter?.saveTitle(title: text)
    }
    
    @IBAction func getTitleAction(_ sender: Any) {
        //Presenterに処理を渡す
        self.presenter?.loadTitle()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        if self.presenter == nil {
            self.presenter = SamplePresenter(viewOutput: self)
        }
    }
}
extension SampleViewController: SampleViewOutput {
    func titleOutput(title: String) {
        titleLabel.text = title
    }
}

5-6. 実行結果

タイトル1を保存してタイトル2に変更しました。



6. まとめ

いかがでしたでしょうか。

今回はMVPアーキテクチャでサンプルアプリを作ってみました。

コードを見ていただくと分かる通り、Model-View-Presenterの役割がはっきりしています。

コードの量も一部のファイルに集中しずらいのでどこに何の処理が有るのかが、わかりやすいと思います。