JY-CONTENTS

JY

JY-CONTENTS
search

+

MENU

デザインパターン(MVC)を使ったテスト②【タスクを作成する画面】

デザインパターン(MVC)を使ったテスト②【タスクを作成する画面】

(DATE)

-

2019.09.16

(CATEGORY)

-

ユーザーがタスクを入力・保存する画面のMVCです。
前回記事のViewControllerクラス(controller層)で設置したナビゲーションバーの+ボタンをタップした時に表示される画面です。
Model層は前回記事ですでに出てきたTaskDataSourceクラスです。

以下の記事の続きになります。
デザインパターン(MVC)を使ったテスト①【タスク一覧を表示する画面】

View層

CreateTaskViewクラス

CreateTaskViewクラスはタスク内容と締め切り日を入力できるUITextFieldと保存ボタンを保持し、入力内容を CreateTaskViewControllerクラス(controller層)に(伝達)渡します。

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クラス内でメソッドの内容を記述するので、前者が委譲元、後者が委譲先になります。

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に保存されます。

NEW TOPICS

/ ニュー & アップデート

SEE ALSO

/ 似た記事を見る

JY CONTENTS UNIQUE BLOG

search-menu search-menu