【タスクを作成する画面】
ユーザーがタスクを入力・保存する画面のMVCです。
前回記事のViewControllerクラス(controller層)で設置したナビゲーションバーの+ボタンをタップした時に表示される画面です。
Model層は前回記事ですでに出てきたTaskDataSourceクラスです。
以下の記事の続きになります。
デザインパターン(MVC)を使ったテスト①【タスク一覧を表示する画面】
View層
CreateTaskViewクラス
CreateTaskViewクラスはタスク内容と締め切り日を入力できるUITextFieldと保存ボタンを保持し、入力内容を CreateTaskViewControllerクラス(controller層)に(伝達)渡します。
● CreateTaskViewクラス/CreateTaskViewDelegateプロトコル
import UIKit
//保存ボタンやピッカーがタップされた時の挙動を示すプロトコル
//delegateパターンなどで、protocolを使用する際には、委譲元に定義する。
protocol CreateTaskViewDelegate: class {
func createView(taskEditting view: CreateTaskView, text: String)
func createView(deadlineEditting view: CreateTaskView, deadline: Date)
func createView(saveButtonDidTap view: CreateTaskView)
}
//CreateTaskViewControllerクラス内でインスタンス生成されるクラス
class CreateTaskView: UIView {
//タスクの内容を入力
var taskField: UITextField!
//締切時間を入力
var dedlineField: UITextField!
//締切時間を表示するピッカー
var datePicker: UIDatePicker!
//保存ボタン
var saveButton: UIButton!
//CreateTaskViewControllerクラスにて設定
weak var delegate: CreateTaskViewDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
//---タスクの内容入力フィールド--------------------
taskField = UITextField()
//UITextFieldの設定をこのクラスで行う
taskField.delegate = self //委譲先をselfに指定
//dedlineFieldと区別するためのtag設定
//tagプロパティはUITextFieldが継承しているUIViewのプロパティ
taskField.tag = 0
taskField.placeholder = "タスク内容を入力してください"
self.addSubview(taskField)
//-------------------------------------------
//---タスクの締切時間入力フィールド--------------------
dedlineField = UITextField()
dedlineField.tag = 1 //taskFieldと区別するためのtag設定
dedlineField.placeholder = "締切時間を入力してください"
self.addSubview(dedlineField)
//ピッカーの設定
datePicker = UIDatePicker()
datePicker.datePickerMode = .dateAndTime
datePicker.addTarget(self, action: #selector(datePickerValueChanged(_:)),
for: .valueChanged)
//締切時間のUITextFieldが編集モードになった時、キーボードではなく、UIDatePickerになるようにする
dedlineField.inputView = datePicker
//------------------------------------------------
//---保存ボタンの設定------------------------------------
saveButton = UIButton()
saveButton.setTitle("保存する", for: .normal)
saveButton.setTitleColor(UIColor.black, for: .normal)
saveButton.layer.borderWidth = 0.5
saveButton.layer.cornerRadius = 4.0
saveButton.addTarget(self, action: #selector(saveBtTap(_:)),
for: .touchUpInside)
self.addSubview(saveButton)
//-----------------------------------------------
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//このクラスのデリゲートのc先であるCreateTaskViewControllerクラスが動作する時に
//delegateプロパティにCreateTaskViewControllerクラスがセットされ、委譲先となっている
//なので各ボタンのタップ時に呼ばれるメソッド内のdelegateプロパティには
//CreateTaskViewControllerクラスがセットされている
//保存ボタンがタップされた時に呼ばれるメソッド
@objc func saveBtTap(_ sender: UIButton) {
//デリゲートメソッドの呼び出し
delegate?.createView(saveButtonDidTap: self)
}
//UIDatePickerの値が変わった時に呼ばれるメソッド
@objc func datePickerValueChanged(_ sender: UIDatePicker) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm"
//設定したフォーマットにユーザーが選択した値(sender.date)を適用
let dedlineText = dateFormatter.string(from: sender.date)
//締切日フィールドに値をセット
dedlineField.text = dedlineText
//デリゲートメソッドの呼び出し
delegate?.createView(deadlineEditting: self, deadline: sender.date)
}
//タスクフィールド、締切日フィールド、保存ボタンをレイアウト
override func layoutSubviews() {
super.layoutSubviews()
taskField.frame = CGRect(x: bounds.origin.x + 30,
y: bounds.origin.y + 30,
width: bounds.size.width - 60,
height: 50
)
dedlineField.frame = CGRect(x: taskField.frame.origin.x,
y: taskField.frame.maxY + 30,
width: taskField.frame.size.width,
height: taskField.frame.size.height
)
let saveButtonSize = CGSize(width: 100, height: 50)
saveButton.frame = CGRect(x: (bounds.size.width - saveButtonSize.width) / 2,
y: dedlineField.frame.maxY + 20,
width: saveButtonSize.width,
height: saveButtonSize.height)
}
}
//UITextFieldDelegateプロトコルを継承
//UITextFieldの設定を行う
extension CreateTaskView: UITextFieldDelegate {
//テキストフィールドが編集中に毎回呼び出されるメソッド
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool{
//タスク入力フィールドが編集中の時
if textField.tag == 0 {
//デリゲートメソッドの呼び出し
delegate?.createView(taskEditting: self, text: textField.text ?? "")
}
return true
}
}
CreateTaskViewDelegateプロトコル
保存ボタンやピッカー、テキストフィールドがタップや編集された時の挙動を示すデリゲートメソッドが実装されています。
CreateTaskViewクラス内で呼び出し(実行)、CreateTaskViewControllerクラス内でメソッドの内容を記述するので、前者が委譲元、後者が委譲先になります。
● CreateTaskViewDelegateプロトコル
protocol CreateTaskViewDelegate: class {
//引数でタスクフィールドの内容を渡す想定
func createView(taskEditting view: CreateTaskView, text: String)
//引数に締切日フィールドの内容を渡す想定
func createView(deadlineEditting view: CreateTaskView, deadline: Date)
func createView(saveButtonDidTap view: CreateTaskView)
}
UIDatePicker
UIDatePickerの具体的な使い方はここでは説明はしません。
以下のコードについて、
dedlineField.inputView = datePicker
通常テキストフィールドが選択状態になった時はキーボードが表示されますが、上記のコードを記述する事で、締切時間フィールドが選択状態になった時にピッカーが表示されるようになります。
saveBtTap(_:)メソッド
保存ボタンがタップされた時に呼び出されるメドッドです。
@objc func saveBtTap(_ sender: UIButton) {
//デリゲートメソッドの呼び出し
delegate?.createView(saveButtonDidTap: self)
}
委譲先CreateTaskViewControllerクラスで実装されているデリゲートメソッドが呼び出されています。
デリゲートメソッドcreateView(saveButtonDidTap:)は、タスクフィールド、締切日フィールドの値をTaskDataSourceクラスのsetDataメソッドを使ってuserDefaultsに保存します。
datePickerValueChanged(_ :)メソッド
締切日フィールドのUIDatePickerの値が変わった時に呼ばれるメソッドです。
@objc func datePickerValueChanged(_ sender: UIDatePicker) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm"
//設定したフォーマットにユーザーが選択した値(sender.date)を適用
let dedlineText = dateFormatter.string(from: sender.date)
//締切日フィールドに値をセット
dedlineField.text = dedlineText
//デリゲートメソッドの呼び出し
delegate?.createView(deadlineEditting: self, deadline: sender.date)
}
ユーザーがピッカーで選択した値を、設定したフォーマットに適用して、それを締切日フィールドにセットします。
委譲先CreateTaskViewControllerクラスで実装されているデリゲートメソッドが呼び出されています。
デリゲートメソッドcreateView(deadlineEditting:)は引数で取ったピッカーの値をCreateTaskViewControllerクラスのtaskDeadlineプロパティにセットします。
taskDeadlineプロパティにセットされた値は「保存ボタン」をタップする事でデリゲートメソッドcreateView(saveButtonDidTap:)が呼び出され保存されます。
UITextFieldDelegateプロトコルを継承
CreateTaskViewクラスにUITextFieldの設定を委譲しているので、UITextFieldDelegateプロトコルを継承します。
デリゲートメソッドtextField(_:shouldChangeCharactersIn:replacementString:)はテキストフィールドが編集中に毎回呼び出されるメソッドです。
extension CreateTaskView: UITextFieldDelegate {
//テキストフィールドが編集中に毎回呼び出されるメソッド
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool{
//タスク入力フィールドが編集中の時
if textField.tag == 0 {
//デリゲートメソッドの呼び出し
delegate?.createView(taskEditting: self, text: textField.text ?? "")
}
return true
}
}
tagプロパティの値を用いて、タスク入力フィールドが編集中なのかを判断しています。
委譲先CreateTaskViewControllerクラスで実装されているデリゲートメソッドが呼び出されています。
デリゲートメソッドcreateView(taskEditting:)は引数で取ったタスクフィールドの値をCreateTaskViewControllerクラスのtaskTextプロパティにセットします。
taskTextプロパティにセットされた値は「保存ボタン」をタップする事でデリゲートメソッドcreateView(saveButtonDidTap:)が呼び出され保存されます。
controller層
CreateTaskViewControllerクラス
CreateTaskViewControllerクラスはCreateTaskViewから受け取った入力情報をTaskDataSource.setData()メソッドを用いて保存します。
import UIKit
class CreateTaskViewController: UIViewController {
//view層のクラス
fileprivate var createTaskView: CreateTaskView!
//model層のクラス
fileprivate var dataSource: TaskDataSource!
fileprivate var taskText: String?
fileprivate var taskDeadline: Date?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
//view層のインスタス生成
createTaskView = CreateTaskView()
//CreateTaskViewDelegateプロトコルを継承してCreateTaskViewクラスの移譲先になる
createTaskView.delegate = self
view.addSubview(createTaskView)
//model層のインスタス生成
dataSource = TaskDataSource()
}
//createTaskViewのレイアウト
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
createTaskView.frame = CGRect(x: view.safeAreaInsets.left,
y: view.safeAreaInsets.top,
width: view.frame.size.width - view.safeAreaInsets.left - view.safeAreaInsets.right,
height: view.frame.size.height - 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)
present(alertController, animated: true, completion: nil)
}
//保存ボタンがタップされた時、タスクフィールド未入力時のアラート
fileprivate func showMissingTaskTextAlert() {
let alertController = UIAlertController(title: "タスクを入力してください",
message: nil,
preferredStyle: .alert
)
let action = UIAlertAction(title: "OK", style: .cancel, handler: nil)
alertController.addAction(action)
present(alertController, animated: true, completion: nil)
}
//保存ボタンがタップされた時、締切日フィールド未入力時のアラート
fileprivate func showMissingTaskDeadlineAlert() {
let alertController = UIAlertController(title: "締切日を入力してください",
message: nil, preferredStyle: .alert
)
let action = UIAlertAction(title: "OK", style: .cancel, handler: nil)
alertController.addAction(action)
present(alertController, animated: true, completion: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
//control層はmodel層とview層の橋渡し役なので、このデリゲートメソッドで橋渡し役をしている
//CreateTaskViewクラスの移譲先になっているのでCreateTaskViewDelegateプロトコルを継承
//保存ボタンやピッカーが選択やタップされた時の挙動をメソッド内に記述
extension CreateTaskViewController: CreateTaskViewDelegate {
//タスク入力フィールドが編集中の時に呼び出される
//引数textにはタスク入力フィールドに入力された値が渡される
func createView(taskEditting view: CreateTaskView, text: String) {
taskText = text
}
//UIDatePickerの値が変わった時に呼び出される
//引数deadlineにはピッカーで選択されている値が渡される
func createView(deadlineEditting view: CreateTaskView, deadline: Date) {
taskDeadline = deadline
}
//保存ボタンがタップされた時に呼び出される
func createView(saveButtonDidTap view: CreateTaskView) {
guard let taskText = taskText else {
//taskTextプロパティが空の場合のアラート
showMissingTaskTextAlert()
return
}
guard let taskDeadline = taskDeadline else {
//taskDeadlineプロパティが空の場合のアラート
showMissingTaskDeadlineAlert()
return
}
//タスクフィールド、締切日フィールドに入力された値でTaskインスタンスを生成
let task = Task(text: taskText, dedline: taskDeadline)
//生成したTaskインスタンスを保存
dataSource.setData(task)
//保存が完了した時のアラート
showSaveAlert()
}
}
safeAreaInsetsについて
viewDidLayoutSubviews()メソッド内で使っているsafeAreaInsetsについて、
SafeAreaの上下左右の値は、 safeAreaInsets から取得することができます。
(※ViewDidLoad内では取得できないようです。)
画面が回転するときはviewSafeAreaInsetsDidChange()で回転後のサイズを取り出せますがwillAnimateRotation()でも可能です。
SafeAreaの概要については以下の記事が解り易いです。
SafeAreaについてまとめ
CreateTaskViewDelegateプロトコルの継承
control層はmodel層とview層の橋渡し役なので、このデリゲートメソッドで橋渡し役をしています。
CreateTaskViewクラスの移譲先になっているのでCreateTaskViewDelegateプロトコルを継承し、デリゲートメソッドの内容を記述します。
extension CreateTaskViewController: CreateTaskViewDelegate {
//タスク入力フィールドが編集中の時に呼び出される
//引数textにはタスク入力フィールドに入力された値が渡される
func createView(taskEditting view: CreateTaskView, text: String) {
taskText = text
}
//UIDatePickerの値が変わった時に呼び出される
//引数deadlineにはピッカーで選択されている値が渡される
func createView(deadlineEditting view: CreateTaskView, deadline: Date) {
taskDeadline = deadline
}
//保存ボタンがタップされた時に呼び出される
func createView(saveButtonDidTap view: CreateTaskView) {
guard let taskText = taskText else {
//taskTextプロパティが空の場合のアラート
showMissingTaskTextAlert()
return
}
guard let taskDeadline = taskDeadline else {
//taskDeadlineプロパティが空の場合のアラート
showMissingTaskDeadlineAlert()
return
}
//タスクフィールド、締切日フィールドに入力された値でTaskインスタンスを生成
let task = Task(text: taskText, dedline: taskDeadline)
//生成したTaskインスタンスを保存
dataSource.setData(task)
//保存が完了した時のアラート
showSaveAlert()
}
}
● 橋渡し役としての役割
まず、
- CreateTaskViewクラス(view層)で実装されているタスクフィールドに入力された値をtaskTextプロパティにセット。
- CreateTaskViewクラスで(view層)実装されている締切日フィールドに入力された値をtaskDeadlineプロパティにセット。
します。
タスクフィールド、締切日フィールドの値をCreateTaskViewControllerクラス(controll層)にて保持します。
view層の値をcontroll層で保持する感じです。
そして、
- CreateTaskViewクラス(view層)で実装されている保存ボタンをタップするとtaskTextプロパティとtaskDeadlineプロパティの値がTaskDataSourceクラス(model層)に実装されているuserdefaultsに保存されます。