はじめに
本記事ではiOSアプリ実装では最もよく用いられているであろうCocoa MVCパターンについて解説します。
巷ではクリーンアーキテクチャーはじめ様々なソフトウェアアーキテクチャーが出てきていますが、Swiftでアプリケーションを構築する上で余程大規模にならない限りは、Apple自体が推していることもあり、このCocoa MVCパターンを使うのが自分の経験上最もいいのではないかなと思います。
ソフトウェアアーキテクチャーは本などで読むとふむふむなるほどと思うのですが、いざ実装しようとしてみると難しいことが多いです。
そこで今回は実際に非常に単純なアプリケーションを実装しながら手を動かして理解するような構成にしています。
では早速始めていきましょう!
Cococa MVCパターンについてのイメージを掴む
何も知らない状態で手を動かしてもあまり学習効率が良くないので、先にCocoa MVCのイメージを掴んでおきましょう。
実際手を動かした後もう一度復習するので、この時点ではこんなもんだなーくらいの理解で構いません。
MVCパターンとはModel-View-Controller Patternの略で、その名の通りアプリケーション内のコードをModel、View、Controllerの3つの役割に分割します。
Model
Modelはデータの保持及び処理を担当します。
具体的には通信処理や計算処理、ローカルストレージに対する保存処理など、後述のView、Controller以外全てがModelに含まれます。RailsなどのWebアプリのフレームワークにおけるSQLデータベース上のテーブルを表現するModelとは意味が異なるので注意が必要です。
またModelは保持しているデータが変更されたら、そのModelを購読しているControllerに変更を通知します。
View
Viewは画面の描画処理を行います。
再利用性を高めるためにも極力ロジックは含めずに、入力を受けてそれをそのまま描画するような設計にすることが重要です。
Controller
ControllerはModelとViewの参照を保持し、Modelについては変更を監視します。
Controllerはユーザーの入力を受けつけ、Modelに処理を依頼し、Modelが変更されたのを検知してViewの描画を更新します。
これらをまとめると以下のようになります。
名前 | 役割 |
---|---|
Model | データ保持、処理を行う |
View | 画面描画を行う |
Controller | ユーザーの入力を受け付ける。ModelとControllerの橋渡し役となりアプリ全体をコントロールする。 |
ここで非常に重要なポイントはControllerがViewとModelの橋渡し役となることで、ViewとModelが何かに依存せずに完全に独立しているところです。
こうすることで、ViewとModelは様々なところで再利用することができるようになります。
逆にControllerは特定のViewとModelに強く依存しており、Cocoa MVCは
Controllerの再利用性を犠牲にViewとModelの再利用性を極限まで高めた設計と言えます。
そして実際にユーザーの入力〜画面の描画までの全体の処理の流れは以下のようになります。
- [Controller] ユーザーの入力を受け付ける。
- [Controller] Modelに処理を依頼する。
- [Model] データを処理する。
- [Model] データの処理結果を購読しているControllerに通知する
- [Controller] Modelの変更を検知する。
- [Controller] Viewに描画を指示する。
- [View] 画面に描画を行う。
ではここままででざっくりイメージがつかめたと思うので、次項から実際に手を動かして実装していきましょう!
手を動かして実装する
今回作成するアプリは+ボタン、-ボタンで数字を増減させることができるシンプルなカウンターアプリです。
こちらをCocoa MVCパターンを使って実装していきましょう。
ゼロからプロジェクト作成をしていただいても構いませんし、初期の状態と完成形をこちらに用意しておいたので、こちらをCloneしてJP/Starterから始めても構いません。
https://github.com/kazuooooo/CocoaMVCFromScratch
CounterModelを実装する
まずはMVCのM、ModelにあたるCounterModelを実装していきましょう。
前述の通りデータの保持、処理を行うのがModelの役割なので、
- 今数値がいくらなのか保持する(データの保持)
- 数字を増やす/減らす(データの処理)
- Modelを監視しているコントローラーに変更を通知する
が必要です。
import Foundation
class CounterModel {
static let notificationName = "CounterModelChanged"
let notificationCenter = NotificationCenter()
// 今数値がいくらなのか保持する(データの保持)
internal var count: Int = 0 {
didSet {
// Modelを監視しているコントローラーに変更を通知する
notificationCenter.post(
name: .init(rawValue: CounterModel.notificationName),
object: count
)
}
}
// 今数値がいくらなのか保持する(データの保持)
func countUp(){ count += 1 }
func countDown(){ count -= 1 }
}
CounterViewを実装する
続いてMVCのVにあたるCounterViewを実装します。
Viewの役割は描画処理を行うことです。
CountViewはrenderというメソッドを通してcountLabelに描画処理を行います。
import Foundation
import UIKit
class CounterView: UIView {
@IBOutlet weak var countLabel: UILabel!
public func render(count: Int){
countLabel.text = String(count)
}
}
続いて実際の画面をStoryBoardで作成してください。(Starterを使っている場合は事前に作成してあります。)
カウントのLabelはIBOutletで接続し、親のViewにCounterViewを設定します。
CounterViewControllerを実装する
最後にMVCのC、CounterViewControllerを実装します。
ControllerではViewとModelの橋渡しをするために
- Modelの変更を監視する
- ユーザーの入力を受け付けて、Modelに処理を依頼する
- Modelの変更を検知して、Viewに描画処理を依頼する
を行う必要があります。
実装は以下のようになります。
import UIKit
class CounterViewController: UIViewController {
// ViewとModelの参照を保持する
@IBOutlet var counterView: CounterView!
private(set) lazy var counterModel = CounterModel()
override func viewDidLoad() {
super.viewDidLoad()
// Modelの変更を監視する
counterModel.notificationCenter.addObserver(
self,
selector: #selector(self.handleCountChange(_:)),
name: .init(NSNotification.Name(rawValue: CounterModel.notificationName)), object: nil
)
}
// 変更を検知する
@objc func handleCountChange(_ notification: Notification) {
if let count = notification.object as? Int {
// Viewに描画処理を依頼する
counterView.render(count: count)
}
}
// 入力を受け付ける
@IBAction func OnPlusButtonTapped(_ sender: Any) {
// Modelに処理を依頼する
counterModel.countUp()
}
@IBAction func OnMinusButtonTapped(_ sender: Any) {
counterModel.countDown()
}
}
こちらもボタンのIBActionへの紐付け、ViewControllerクラスの設定を忘れないようにしましょう。
さて以上で実装は完了です。
一度Runをして+/-ボタンが正しく動くか確認してみてください!
再度パターンに当てはめて考えてみる
実装は完了しましたが、写経しただけ感があってまだどこかしっくりきていませんよね?
最後にもう一度全体像をコードを見ながら確認していきましょう。
そうすることで理解がグッと深まるはずです。
依存関係を見てみる
まずは依存関係について先ほど見たこちらの図を見ながらコードをもう一度確認してみましょう。
Controllerのコードを見てみると、ViewとModelの参照及び、変更の監視がされていることがわかります。
class CounterViewController: UIViewController {
// ViewとModelの参照を保持する
@IBOutlet var counterView: CounterView!
private(set) lazy var counterModel = CounterModel()
override func viewDidLoad() {
super.viewDidLoad()
// Modelの変更を監視する
counterModel.notificationCenter.addObserver(
self,
selector: #selector(self.handleCountChange(_:)),
name: .init(NSNotification.Name(rawValue: CounterModel.notificationName)), object: nil
)
}
...
}
またViewとModelはControllerは処理を受け付けるだけで完全に独立しており、再利用可能なこともコードを見てチェックしておいてください。
処理の流れをチェックする
最後に+ボタンを押した時を例に全体の処理の流れも図とコードを見ながらチェックしていきます。
1.入力を受け付ける
// 入力を受け付ける
@IBAction func OnPlusButtonTapped(_ sender: Any) {
...
}
2. Modelに処理を依頼する
入力を受けたらControllerはModelに処理を依頼します。
@IBAction func OnPlusButtonTapped(_ sender: Any) {
// 2. Modelに処理を依頼する
counterModel.countUp()
}
3. データを処理する
Modelはデータを処理します。
今回の場合はcountUpが呼び出されているので自身のcountをインクリメントします。
class CounterModel {
...
// データを処理する
func countUp(){ count += 1 }
}
4. 変更を通知する
ModelはNotificationCenterを通じて購読者に変更を通知します。
class CounterModel {
...
internal var count: Int = 0 {
didSet {
// Modelを監視しているコントローラーに変更を通知する
notificationCenter.post(
name: .init(rawValue: CounterModel.notificationName),
object: count
)
}
}
...
}
5. Modelの変更を検知する
ControllerはModelのNotificationCenterに登録しておいたObserverからModelの変更を検知します。
class CounterViewController: UIViewController {
override func viewDidLoad() {
...
// Modelの変更を監視する
counterModel.notificationCenter.addObserver(
self,
selector: #selector(self.handleCountChange(_:)),
name: .init(NSNotification.Name(rawValue: CounterModel.notificationName)), object: nil
)
}
...
// 変更を検知する
@objc func handleCountChange(_ notification: Notification) {
...
}
}
6.描画指示
@objc func handleCountChange(_ notification: Notification) {
if let _ = notification.object as? Int {
// Viewに描画処理を依頼する
counterView.render(count: counterModel.count)
}
}
7.描画処理を行う
Controllerからの描画指示を受けてViewは描画処理を行います。
class CounterView: UIView {
// 描画処理を行う
public func render(count: Int){
countLabel.text = String(count)
}
}
終わりに
いかがだったでしょうか?
ソフトウェアアーキテクチャーパターンは初めはとっつきにくいですが、一度理解してしまえばエンジニアにとって非常に強力な武器になってくれます。
今回のCocoaMVCのようにきっちり理解できていなくて雰囲気で使っているなーというところは一度腰を据えて自分で手を動かしてみることで理解が一気に深まるのでオススメです!