MVCパターンについて理解する ~iOS開発~

###MVC(Model-View-Controller)
|レイヤー|役割|
|:--|:--|
|Controller|ユーザーの入力を受け、Modelにコマンドを送る|
|Model|コマンドを受けて処理を行い、自信を更新|
|View|Modelの変更を監視し、自信を更新|

#####iOS開発でのMVCの役割

**[Model]**

- データ構造の表現
- Web APIとのやりとり
- ローカルデータベースなどへのデータ永続化
- データの振る舞いに関するロジック

**View**

- UIの表示
- Controllerからデータを受け取りUIに反映させる
- ユーザーインタラクションを認知

**Controller**

- Modelからデータを受け取りViewに渡してUIを更新する
- ライフサイクル処理や画面遷移などの処理を行う

![image.png](https://qiita-image-store.s3.amazonaws.com/0/351115/b6b9d212-3d9b-99ff-c625-b5ecc3427134.png)

###サンプル
タスク管理アプリを例にしております。

####Model層
**Task.swift**: データを表すオブジェクト

```Task.swift

import Foundation

class Task {
    let text: String
    let deadline: Date
    
    init(text: String, deadline: Date) {
        self.text = text
        self.deadline = deadline
    }
    
    init(from dictionary: [String: Any]) {
        self.text = dictionary["text"] as! String
        self.deadline = dictionary["deadline"] as! Date
    }
}
```


**TaskDataManager.swift**: データに関するロジック

```TaskDataManager.swift
import Foundation

class TaskDataManager: NSObject {
    private var tasks = [Task]()
    // UserDefaultsから保存したデータを取得
    func loadData() {
        let userDefaults = UserDefaults.standard
        guard let taskDictionaries = userDefaults.object(forKey: "tasks") as? [[String: Any]] else { return }
        for dic in taskDictionaries {
            let task = Task(from: dic)
            self.tasks.append(task)
        }
    }
    
    func save(task: Task) {
        self.tasks.append(task)
        var taskDictionaries = [[String: Any]]()
        for t in self.tasks {
            let taskDictionary: [String : Any] = ["text": t.text, "deadline": t.deadline]
            taskDictionaries.append(taskDictionary)
        }
        let userDefaults = UserDefaults.standard
        userDefaults.set(taskDictionaries, forKey: "tasks")
        userDefaults.synchronize()
    }
    
    func count() -> Int {
        return self.tasks.count
    }
    
    func data(at index: Int) -> Task? {
        if self.tasks.count > index {
            return tasks[index]
        }
        return nil
    }
}
```

####View層
UITableViewCellを継承したクラス

```TaskCell.swift
import UIKit

class TaskCell: UITableViewCell {
    
    private var taskLabel: UILabel!
    private var deadlineLabel: UILabel!
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        self.taskLabel = UILabel()
        self.taskLabel.textColor = .black
        self.taskLabel.font = UIFont.systemFont(ofSize: 14)
        contentView.addSubview(self.taskLabel)
        
        self.deadlineLabel = UILabel()
        self.deadlineLabel.textColor = .black
        self.deadlineLabel.font = UIFont.systemFont(ofSize: 14)
        contentView.addSubview(self.deadlineLabel)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        self.taskLabel.frame = CGRect(x: 15.0, y: 15.0, width: self.contentView.frame.width - 30, height: 15.0)
        self.deadlineLabel.frame = CGRect(x: self.taskLabel.frame.origin.x, y: self.taskLabel.frame.maxY + 8, width: self.taskLabel.frame.width, height: 15.0)
    }
    
    var task: Task? {
        didSet {
            guard let t = task else { return }
            self.taskLabel.text = t.text
            
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy/MM/dd"
            self.deadlineLabel.text = formatter.string(from: t.deadline)
        }
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }

}
```


```swift:CreateTaskView.swift
import UIKit

// CreateTaskViewControllerへユーザーインタラクションを伝達するためのプロトコル
protocol CreateTaskViewDelegate: class {
    func createView(taskEditting view: CreateTaskView, text: String)
    func createView(deadlineEditting view: CreateTaskView, deadline: Date)
    func createView(saveButtonDidTap view: CreateTaskView)
}

class CreateTaskView: UIView {

    private var taskTextField: UITextField!
    private var datePicker: UIDatePicker!
    private var deadlineTextField: UITextField!
    private var saveButton: UIButton!
    
    weak var delegate: CreateTaskViewDelegate?
    
    required override init(frame: CGRect) {
        super.init(frame: frame)
        
        self.taskTextField = UITextField()
        self.taskTextField.delegate = self
        self.taskTextField.tag = 0
        self.taskTextField.placeholder = "予定を入れてください"
        self.addSubview(self.taskTextField)
        
        self.deadlineTextField = UITextField()
        self.deadlineTextField.tag = 1
        self.deadlineTextField.placeholder = "期限を入れてください"
        self.addSubview(self.deadlineTextField)
        
        self.datePicker = UIDatePicker()
        self.datePicker.datePickerMode = .dateAndTime
        self.datePicker.addTarget(self, action: #selector(datePickerValueChanged(_:)), for: .valueChanged)
        
        // deadlineTextFieldが編集モードになった時に、キーボードではなくUIDatePickerになるようにする
        self.deadlineTextField.inputView = self.datePicker
        
        self.saveButton = UIButton()
        self.saveButton.setTitle("保存する", for: .normal)
        self.saveButton.setTitleColor(.black, for: .normal)
        self.saveButton.layer.borderWidth = 0.5
        self.saveButton.layer.cornerRadius = 4.0
        self.saveButton.addTarget(self, action: #selector(saveButtonTapped(_:)), for: .touchUpInside)
        self.addSubview(self.saveButton)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    @objc func datePickerValueChanged(_ sender: UIDatePicker) {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy/MM/dd HH:mm"
        let deadlineText = dateFormatter.string(from: sender.date)
        self.deadlineTextField.text = deadlineText
        self.delegate?.createView(deadlineEditting: self, deadline: sender.date)
    }
    
    @objc func saveButtonTapped(_ sender: UIButton) {
        self.delegate?.createView(saveButtonDidTap: self)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        self.taskTextField.frame = CGRect(x: bounds.origin.x + 30, y: bounds.origin.y + 30, width: bounds.size.width - 60, height: 50)
        
        self.deadlineTextField.frame = CGRect(x: self.taskTextField.frame.origin.x, y: self.taskTextField.frame.maxY + 30, width: self.taskTextField.frame.size.width, height: self.taskTextField.frame.size.height)
        
        let saveButtonSize = CGSize(width: 100, height: 50)
        self.saveButton.frame = CGRect(x: (bounds.size.width - saveButtonSize.width) / 2, y: self.deadlineTextField.frame.maxY + 20, width: saveButtonSize.width, height: saveButtonSize.height)
        
    }
    
}

extension CreateTaskView: UITextFieldDelegate {
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        delegate?.createView(taskEditting: self, text: textField.text ?? "")
        return true
    }
}
```


####Controller層
ModelとViewの仲介役。
TaskDataManagerからデータを受け取りTaskCellに反映させる

```swift:TaskListViewController.swift
import UIKit

class TaskListViewController: UIViewController {
    
    var dataSource: TaskDataManager!
    var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.dataSource = TaskDataManager()
        self.tableView = UITableView(frame: self.view.bounds, style: .plain)
        self.tableView.register(TaskCell.self, forCellReuseIdentifier: "Cell")
        self.tableView.delegate = self
        self.tableView.dataSource = self
        self.view.addSubview(self.tableView)
        
        let barButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(barButtonTapped(_:)))
        self.navigationItem.rightBarButtonItem = barButton
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.dataSource.loadData()
        self.tableView.reloadData()
    }
    
    @objc func barButtonTapped(_ sender: UIBarButtonItem) {
        // タスク作成画面へ遷移
        let controller = CreateTaskViewController()
        let navi = UINavigationController(rootViewController: controller)
        self.present(navi, animated: true, completion: nil)
    }
    
}

extension TaskListViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.dataSource.count()
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 68.0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? TaskCell else { return UITableViewCell() }
        let task = self.dataSource.data(at: indexPath.row)
        cell.task = task
        return cell
    }
    
}
```

タスク作成画面

```swift:CreateTaskViewController.swift
import UIKit

class CreateTaskViewController: UIViewController {
    
    fileprivate var createTaskView: CreateTaskView!
    
    fileprivate var dataSource: TaskDataManager!
    fileprivate var taskText: String?
    fileprivate var taskDeadline: Date?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.backgroundColor = .white
        self.createTaskView = CreateTaskView()
        self.createTaskView.delegate = self
        self.view.addSubview(self.createTaskView)
        
        self.dataSource = TaskDataManager()
        
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        // createTaskViewのレイアウトを設定
        self.createTaskView.frame = CGRect(
            x: self.view.safeAreaInsets.left,
            y: self.view.safeAreaInsets.top,
            width: self.view.frame.size.width - self.view.safeAreaInsets.left - self.view.safeAreaInsets.right,
            height: self.view.frame.size.height - self.view.safeAreaInsets.bottom
        )
    }
    
    // 保存が成功した時のアラート
    fileprivate func showSaveAlert() {
        let alertController = UIAlertController(title: "保存しました", message: nil, preferredStyle: .alert)
        let action = UIAlertAction(title: "OK", style: .cancel) { action in
            self.navigationController?.popViewController(animated: true)
        }
        alertController.addAction(action)
        self.present(alertController, animated: true, completion: nil)
    }
    
    // タスクが未入力時のアラート
    fileprivate func showMissingTaskTextAlert() {
        let alertController = UIAlertController(title: "タスクを入力してください", message: nil, preferredStyle: .alert)
        let action = UIAlertAction(title: "OK", style: .cancel) { action in }
        alertController.addAction(action)
        self.present(alertController, animated: true, completion: nil)
    }
    
    // タスクが未入力時のアラート
    fileprivate func showMissingTaskDeadlineAlert() {
        let alertController = UIAlertController(title: "締切日を入力してください", message: nil, preferredStyle: .alert)
        let action = UIAlertAction(title: "OK", style: .cancel) { action in }
        alertController.addAction(action)
        self.present(alertController, animated: true, completion: nil)
    }
    
}

extension CreateTaskViewController: CreateTaskViewDelegate {
    
    // タスク内容を入力している時に呼ばれるデリゲートメソッド
    func createView(taskEditting view: CreateTaskView, text: String) {
        // CreateTaskViewからタスク内容を受け取りtaskTextに代入している
        self.taskText = text
    }
    
    // 締切日時を入力している時に呼ばれるデリゲートメソッド
    func createView(deadlineEditting view: CreateTaskView, deadline: Date) {
        self.taskDeadline = deadline
    }
    
    // 保存ボタンが押された時に呼ばれるデリゲートメソッド
    func createView(saveButtonDidTap view: CreateTaskView) {
        guard let taskText = self.taskText else {
            self.showMissingTaskTextAlert()
            return
        }
        guard let taskDeadline = self.taskDeadline else {
            self.showMissingTaskDeadlineAlert()
            return
        }
        let task = Task(text: taskText, deadline: taskDeadline)
        self.dataSource.save(task: task)
        
        self.showSaveAlert()
    }
    
}
```