3
7

More than 3 years have passed since last update.

【Swift】MVVM勉強してみたPart3

Last updated at Posted at 2021-04-07

はじめに

前回に続き、iOSアプリ設計パターン入門のMVVM(第7章)を読んで学んだことを自分なりにまとめていきます!
今回は、「Notification Centerによる実装」で簡単なアプリを作ってみます。次回は同じ動作をするアプリを「RxSwift」で作ってみたいと思います!

ezgif.com-gif-maker (3).gif

GitHub

実装

View

NotificationCenterViewController
import UIKit

final class NotificationCenterViewController: UIViewController {

    @IBOutlet private weak var idTextField: UITextField!
    @IBOutlet private weak var passwordTextField: UITextField!
    @IBOutlet private weak var validationLabel: UILabel!

    private let notificationCenter = NotificationCenter()
    private lazy var viewModel = NotificationCenterViewModel(notificationCenter: notificationCenter)

    override func viewDidLoad() {
        super.viewDidLoad()

        idTextField.addTarget(self, action: #selector(textFieldEditignChanged), for: .editingChanged)
        passwordTextField.addTarget(self, action: #selector(textFieldEditignChanged), for: .editingChanged)
        //自身の状態とViewModelの状態をデーターバインディングする
        notificationCenter.addObserver(self, selector: #selector(updateValidationText), name: viewModel.changeText, object: nil)
        //自身の状態とViewModelの状態をデーターバインディングする
        notificationCenter.addObserver(self, selector: #selector(updateValidationColor), name: viewModel.changeColor, object: nil)

    }

}

private extension NotificationCenterViewController {

    @objc func textFieldEditignChanged(sender: UITextField) {
        //ユーザーの入力をViewModelに渡す
        viewModel.idPasswordChanged(id: idTextField.text, password: passwordTextField.text)
    }

    @objc func updateValidationText(notification: Notification) {
        //ViewModelから返されるイベントを元に描画処理を実行する
        guard let text = notification.object as? String else { return }
        validationLabel.text = text
    }

    @objc func updateValidationColor(notification: Notification) {
        //ViewModelから返されるイベントを元に描画処理を実行する
        guard let color = notification.object as? UIColor else { return }
        validationLabel.textColor = color
    }

}

ViewModel

NotificationCenterViewModel
import UIKit

final class NotificationCenterViewModel {

    let changeText = Notification.Name("changeText")
    let changeColor = Notification.Name("changeColor")
    private let notificationCenter: NotificationCenter
    private let model: NotificationCenterModelProtocol

    init(notificationCenter: NotificationCenter,
         model: NotificationCenterModelProtocol = NotificationCenterModel()) {
        self.notificationCenter = notificationCenter
        self.model = model
    }

    //Viewからイベントを受け取り、Modelの処理を呼び出す
    func idPasswordChanged(id: String?, password: String?) {
        let result = model.validate(idText: id, passwordText: password)
        switch result {
        case .success:
            //Viewからイベントを受け取り、加工して値を更新する
            notificationCenter.post(name: changeText, object: "OK!!!")
            //Viewからイベントを受け取り、加工して値を更新する
            notificationCenter.post(name: changeColor, object: UIColor.green)
        case .failure(let error as ModelError):
            //Viewからイベントを受け取り、加工して値を更新する
            notificationCenter.post(name: changeText, object: error.errorText)
            //Viewからイベントを受け取り、加工して値を更新する
            notificationCenter.post(name: changeColor, object: UIColor.red)
        case _:
            fatalError("Unexpected pattern.")
        }
    }

}

Model

NotificationCenterModel
protocol NotificationCenterModelProtocol {
    func validate(idText: String?, passwordText: String?) -> Result<Void>
}

final class NotificationCenterModel: NotificationCenterModelProtocol {

    func validate(idText: String?, passwordText: String?) -> Result<Void> {
        switch (idText, passwordText) {
        case (.none, .none): return .failure(ModelError.invalidIdAndPassword)
        case (.none, .some): return .failure(ModelError.invalidId)
        case (.some, .none): return .failure(ModelError.invalidPassword)
        case (let idText?, let passwordText?):
            switch (idText.isEmpty, passwordText.isEmpty) {
            case (true, true): return .failure(ModelError.invalidIdAndPassword)
            case (false, false): return .success(())
            case (true, false): return .failure(ModelError.invalidId)
            case (false, true): return .failure(ModelError.invalidPassword)
            }
        }
    }

}
ModelError
enum Result<T> {
    case success(T)
    case failure(Error)
}

enum ModelError: Error {
    case invalidId
    case invalidPassword
    case invalidIdAndPassword
    var errorText: String {
        switch self {
        case .invalidIdAndPassword: return "IDとPasswordが未入力です。"
        case .invalidId: return "IDが未入力です。"
        case .invalidPassword: return "Passwordが未入力です。"
        }
    }
}

解説

View

Viewの責務は以下の3つです。

・ユーザーの入力をViewModelに渡す
ユーザー入力から始まるプレゼンテーションロジックをViewModelの責務として実現するため、Viewからユーザー入力をViewModelに渡す。(textFieldへの入力をViewModelに渡す)
文字列が入力された時に呼ぶメソッドをそれぞれaddTargetで登録しておき、textFieldEditignChangedでIDまたはパスワードの文字列をviewModelに伝える。

・自身の状態とViewModelの状態をデーターバインディングする
ViewModelからViewへの通知にNotificationCenterを使ってデータバインディングを行う。
ViewModelからの通知をViewが監視し、そのイベントでViewが画面を更新できるようにする。
つまり、ViewModel側で定義したNotification.Name(viewModel.changeTextとviewModel.changeColor)がviewModelでpostされるたびに、updateValidationText(notification:)updateValidationColor(notification:)が呼び出されるようにする。

・ViewModelから返されるイベントを元に描画処理を実行する
Notification Centerで受け取った通知を使い、Viewで実際に画面を更新する。
データバインディングしたイベントで描画を実行するメソッドを定義。

ViewModel

ViewModelの責務は以下の3つです。

・Viewに表示するためのデータを保持する
プレゼンテーションロジックの判定に使ったり、ViewModel内でデータを加工したりするために、ViewModelはViewに表示するためのデータを保持する。(今回は状態を保持する必要がないため登場しない)

・Viewからイベントを受け取り、Modelの処理を呼び出す
Viewが呼ぶidPasswordChanged(id:password)を元に、Modelを呼び出したりModelから新たな値を取得したりする。
コードではtextFieldの文字列の入力イベントに対して、modelのvalidate(idText:passwordText:)を呼んでバリデーション結果を取得している。

・Viewからイベントを受け取り、加工して値を更新する
Viewからイベントを受け取り、自身がもつ値を加工して表示を更新させることもViewModelの役割。
Modelから受け取った値を加工することもあればModelと関わりのない表示上のデータ加工であればModelを利用せず、ViewModelの内部で処理して描画処理を行うこともある。
コードではidPasswordChanged(id:password:)のなかでModelから取得した値を元にデータを加工している。加工した結果の文字列と色はNotificationCenterのpost(name:object:)によって通知する。これにより、監視しているViewControllerに伝わる。

Model

Modelはプレゼンテーションロジック以外のドメインロジックを担当する。
今回のアプリでは、文字列のバリデーションがドメインロジック。Modelはその判定処理を担っている。

今回追加で学んだこと

@objc funcextensionで分けておくと、みやすくなる
addObserverを通知を受け取りたい側で登録することで、Notification.Name("SomeString")通知が発行されたタイミングでセレクタ関数が実行される。
NotificationCenterpostメソッドでViewで登録しておいたNotification.Name("SomeString")の値を更新できる。

おわりに

次回
少しずつ理解できてきました!!!

3
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
7