4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

###MVC(Model-View-Controller)

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

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

[Model]

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

View

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

Controller

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

image.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)
    }

}
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に反映させる

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
    }
    
}

タスク作成画面

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()
    }
    
}
4
4
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
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?