###MVC(Model-View-Controller)
レイヤー | 役割 |
---|---|
Controller | ユーザーの入力を受け、Modelにコマンドを送る |
Model | コマンドを受けて処理を行い、自信を更新 |
View | Modelの変更を監視し、自信を更新 |
#####iOS開発でのMVCの役割
[Model]
- データ構造の表現
- Web APIとのやりとり
- ローカルデータベースなどへのデータ永続化
- データの振る舞いに関するロジック
View
- UIの表示
- Controllerからデータを受け取りUIに反映させる
- ユーザーインタラクションを認知
Controller
- Modelからデータを受け取りViewに渡してUIを更新する
- ライフサイクル処理や画面遷移などの処理を行う
###サンプル
タスク管理アプリを例にしております。
####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()
}
}