JY-CONTENTS

JY

JY-CONTENTS
search

+

MENU

デザインパターン(MVC)を使ったテスト①【タスク一覧を表示する画面】

デザインパターン(MVC)を使ったテスト①【タスク一覧を表示する画面】

(DATE)

-

2019.09.15

(CATEGORY)

-

MVCを使ったタスクを管理するテストコードです。

【タスク一覧を表示する画面】
保存されたデータ(タスク)を一覧表示する為のMVCです。

Model層

Model層はデータに関する処理等、表示や入力に関連しない処理を実装します。
今回はタスクデータの管理になります。

Taskクラス

Taskクラスはデータ構造を記述しています。
「タスクの内容」「タスクの締切日」の2つのデータを保有しています。

import Foundation

class Task {
    let text:String //タスクの内容
    let dedline:Data //タスクの締切日
    
    init(text:String, dedline:Data){
        self.text = text
        self.dedline = dedline
    }
    
    //UserDefaultで保存したデータを受け取る
    //UserDefaultで保存したデータからの生成
    init(dict: [String: Any]){
        self.text = dict["text"] as! String
        self.dedline = dict["dedline"] as! Data
    }
}

TaskDataSourceクラス

TaskDataSourceクラスはデータの行いや処理を記述しています。
「Taskクラス(データ保有)を保存」する処理と、「保存したTaskクラスを取り出す」処理です。

UserDefaultsへの保存方法(2種類)

UserDefaultsへTaskクラスのような独自クラスを保存する場合、独自クラスを一旦NSData型にする必要があり、その為に独自クラスにNSCodingプロトコルを実装する方法があります。
今回はプロトコルを実装する方法としない方法を記述します。

【swift】UserDefaults

プロトコルを実装しない方法

独自クラスのインスタンスを別の型(array型)に置き換えて配列に保存することで、その配列をuserDefaultsに保存します。array型はuserDefaultsにて保存可能です。

※)独自クラスのインスタンスをそのまま配列に保存した場合、その配列はuserDefaultsには保存できません。

import Foundation

class TaskDataSource {
    //getData()メソッドを実行して取得したTaskインスタンスを保持する為の配列
    var tasks = [Task]()

    //UserDefaultsのインスタンス取得
    let userDefaults = UserDefaults.standard
    
    
    //TaskをUserDefaultsに保存するメソッド
    func setData(_ task: Task){
        //引数で渡されるTaskインスタンスを保持
        //このメソッドが呼ばれる度にTaskインスタンスがどんどん配列に追加されていく事になる
        tasks.append(task)
        
        //Taskの保有している値を[String: Any]に置き換え、配列に保存
        //この配列をuserDefaultsに保存する
        var taskDictArray = [[String: Any]]()
        
        //配列tasksにあるTaskインスタンスの内容を[String: Any]型にして、配列taskDictArrayに追加
        for task in tasks {
            //Taskインスタンスの保有している値を[String: Any]型にする
            let dict:[String: Any] =  ["text": task.text, "dedline": task.dedline]
            //配列taskDictArrayに保存
            taskDictArray.append(dict)
        }
        
        //配列taskDictArrayをuserDefaultsに保存
        userDefaults.set(taskDictArray, forKey: "tasks")
        userDefaults.synchronize()
    }
    
    
    //UserDefaultsに保存したデータを取得するメソッド
    func getData(){
        //setData(_ task:)メソッドでUserDefaultsに保存された配列taskDictArrayを取得
        guard let taskDictArray = userDefaults.object(forKey: "tasks") as? [[String: Any]] else {
            return
        }

        //setData()で追加したTaskインスタンスとgetData()で追加するTaskインスタンスの
        //内容は同じなのでダブらないように初期化する
        tasks = [Task]()

        for taskDict in taskDictArray{
            //[String: Any]型のデータを引数で渡し、Taskインスタンスを生成します。
            //[String: Any]型に置き換えられたデータを元(Taskインスタンス)に戻す感じ
            let task = Task(dict: taskDict)
            //配列tasksにインスタンスを保存
            tasks.append(task)
        }
    }
    
    
    //配列taskに保存されているTaskインスタンスの合計数を取得(タスク数)
    func count() ->Int {
        return getTasks.count
    }
    
    
    //引数のindex番号のTaskインスタンスを返す
    func indexData(_ index: Int) ->Task? {
        if getTasks.count > index {
            return getTasks[index]
        }
        return nil
    }
}

setData(_ task:)メソッド

引数で渡されたTaskインスタンスを型変換してUserDefaultsに保存するメソッドです。

func setData(_ task: Task){
     //引数で渡されるTaskインスタンスを保持
     //このメソッドが呼ばれる度にTaskインスタンスがどんどん配列に追加されていく事になる
     tasks.append(task)
        
     //Taskの保有している値を[String: Any]に置き換え、配列に保存
     //この配列をuserDefaultsに保存する
     var taskDictArray = [[String: Any]]()
        
     //配列tasksにあるTaskインスタンスの内容を[String: Any]型にして、配列taskDictArrayに追加
     for task in tasks {
         //Taskインスタンスの保有している値を[String: Any]型にする
         let dict:[String: Any] =  ["text": task.text, "dedline": task.dedline]
         //配列taskDictArrayに保存
         taskDictArray.append(dict)
     }
        
     //配列taskDictArrayをuserDefaultsに保存
     userDefaults.set(taskDictArray, forKey: "tasks")
     userDefaults.synchronize()
}

TaskクラスにNSCodingプロトコルを実装していないのでuserDefaultsにTaskインスタンスを直接は保存できません。
なので、Taskインスタンスのデータ(プロパティ)を[String: Any]型に置き換えて、その置き換えたデータを配列に保存し、その配列をuserDefaultsに保存しています。
userDefaultsにはTaskインスタンスを保存するのではなく、[String: Any]型のデータが格納された配列を保存することになります。

保存される配列は以下のようになります。

[
   {
      "text": "打ち合わせ"
      "dedline": "2018-03-17 03:30:00"
   },
   {
      "text": "食事会"
      "dedline": "2018-04-01 06:30:00"
   }
]

getDataメソッド

UserDefaultsに保存したデータを元にTaskインスタンスを取得するメソッドです。

func getData(){
     //setData(_ task:)メソッドでUserDefaultsに保存された配列taskDictArrayを取得
     guard let taskDictArray = userDefaults.object(forKey: "tasks") as? [[String: Any]] else {
         return
     }

     //setData()で追加したTaskインスタンスとgetData()で追加するTaskインスタンスの
     //内容は同じなのでダブらないように初期化する
     tasks = [Task]()

     for taskDict in taskDictArray{
         //[String: Any]型のデータを引数で渡し、Taskインスタンスを生成します。
         //[String: Any]型に置き換えられたデータを元(Taskインスタンス)に戻す感じ
         let task = Task(dict: taskDict)
         //配列tasksにインスタンスを保存
         tasks.append(task)
     }
}

userDefaultsに保存されている、[String: Any]型のデータを元にTaskインスタンスを生成して、配列tasksにインスタンスを保存します。

プロトコルを実装する方法

NSCodingプロトコルは保存する独自クラスに実装する必要があります。
なので今回はTaskクラスに実装します。

Taskクラス

class Task: NSObject, NSCoding {
    let text: String
    let deadline: Date

    func encode(with aCoder: NSCoder) {
        aCoder.encode(text, forKey: "text")
        aCoder.encode(deadline, forKey: "deadline")
    }

    required init?(coder aDecoder: NSCoder) {
        text = aDecoder.decodeObject(forKey: "text") as! String
        deadline = aDecoder.decodeObject(forKey: "deadline") as! Date
    }
}

TaskDataSourceクラス

setData(_ task:)メソッド、getDataメソッドの内容を変更しています。

class TaskDataSource {
    //Taskインスタンスを一覧で保持する為の配列を用意
    var tasks = [Task]()
    //UserDefaultsのインスタンス取得
    let userDefaults = UserDefaults.standard
    
    
    //TaskをUserDefaultsに保存するメソッド
    func setData(_ task: Task){
        //引数で渡されるTaskインスタンスは一覧に保持
        tasks.append(task)
        
        let encodedTask = NSKeyedArchiver.archivedData(withRootObject: tasks)
        let userDefaults = UserDefaults.standard
        userDefaults.set(encodedTask, forKey: "tasks")
        userDefaults.synchronize()
    }
    
    
    //UserDefaultsに保存したデータを取得するメソッド
    func getData(){
        let taskData = userDefaults.object(forKey: "tasks") as? Data
        guard let t = taskData else { return }
        let unArchivedData = NSKeyedUnarchiver.unarchiveObject(with: t) as? [Task]
        tasks = unArchivedData ?? [Task]()
    }
    
    
    //配列taskに保存されているTaskインスタンスの合計数を取得
    func count() ->Int {
        return tasks.count
    }
    
    
    //引数のindex番号のTaskインスタンスを返す
    func indexData(_ index: Int) ->Task? {
        if tasks.count > index {
            return tasks[index]
        }
        return nil
    }
}

この方法についての詳細は以下の記事をご覧ください。

【swift】UserDefaults

countメソッド

countメソッドはViewControllerクラスでセル(タスク)のカウントに使用します。

//ViewControllerクラス

let dataSource = TaskDataSource()

//セル(タスク)の数を返すメソッド
func tableView(_ tableView: UITableView,
  numberOfRowsInSection section: Int) -> Int {
    return dataSource.count()
}

indexData(_ index:)メソッド

indexData(_ index:)メソッドはViewControllerクラスのメソッド内でIndexPath.rowに応じたTaskクラスのインスタンスを取得する為に使用します。

//ViewControllerクラス

let dataSource = TaskDataSource()

//セルに表示するデータを設定するメソッド
func tableView(_ tableView: UITableView,
   cellForRowAt indexPath: IndexPath) -> UITableViewell {
      //セルを作成。viewDidLoad内で紐付けしたセル(TaskListCellクラス)を使用
      let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! TaskListCell

      //index番号に応じたデータ(Taskインスタンス)を取得
      let task = dataSource.indexData(indexPath.row)

      //セル(TaskListCellクラス)のtaskプロパティにデータ(Taskインスタンス)をセット
      cell.task = task
      return cell
}

データを保存する

データを保存してみます。

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let task1 = Task(text: "打ち合わせ", dedline: Date())
        let task2 = Task(text: "食事", dedline: Date())
        let dataSource = TaskDataSource()
        
        //タスク保存
        dataSource.setData(task1)
        dataSource.setData(task2)
        
        //保存したタスクの取得
        dataSource.getData()
        
        //出力
        print("カウント: \(dataSource.count())")
        for t in dataSource.tasks{
            print("\(t.text): \(t.dedline)")
        }
    }
    
    override func viewDidLayoutSubviews() {
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
 
}

View層

View層はModel層のデータをController層から受け取りデバイスに表示する処理を実装します。

TaskListCellクラス

TaskListCellクラスはController層から受け取ったデータをUITableViewのセルに反映(表示)させます。
UITableViewに表示させるセルを設定、作成するクラスです。

import UIKit
//import Foundation

class TaskListCell: UITableViewCell {
    //taskの内容を表示させる
    var taskLabel: UILabel!
    //dedlineを表示させる
    var dedlineLabel: UILabel!
    
    //ViewControllerクラス(Controller層)から値がセットされる
    var task: Task? {
        //値がセットされた後に実行される
        //渡されたTaskインスタンスの値をラベルに反映
        didSet{
            if let t = task {
                taskLabel.text = t.text
                
                let formatter = DateFormatter()
                formatter.dateFormat = "yyyy/MM/dd"
                dedlineLabel.text = formatter.string(from: t.dedline)
            }else{
                return
            }
        }
    }
    
    override init(style: UITableViewCellStyle, reuseIdentifier: String?){
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        taskLabel = UILabel()
        taskLabel.font = UIFont.systemFont(ofSize: 14)
        //セルにラベルを追加
        //contentViewはセルを使って表示させるコンテンツのデフォルトのスーパービュー
        contentView.addSubview(taskLabel)
        
        dedlineLabel = UILabel()
        dedlineLabel.font = UIFont.systemFont(ofSize: 14)
        contentView.addSubview(dedlineLabel)
        
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    //セル内の各ラベルのレイアウト
    override func layoutSubviews() {
        super.layoutSubviews()
        
        taskLabel.frame = CGRect(x: 15,
                                 y: 15,
                                 width: contentView.frame.width - 30,
                                 height: 15)
        
        dedlineLabel.frame = CGRect(x: taskLabel.frame.origin.x,
                                    y: taskLabel.frame.maxY + 8,
                                    width: taskLabel.frame.width,
                                    height: 15)
    }
    
}

contentviewについて

taskプロパティ(プロパティオブザーバ)

プロパティオブザーバはストアドプロパティの値の変更を監視し、変更前と変更後に文を実行するものです。
didSetキーワードは変更後に実行する文を指定します。
ViewControllerクラス(Controller層)のtableView(_ cellForRowAt indexPath:)メソッドでtaskプロパティに値がセットされます。

//ViewControllerクラス(Controller層)から値がセットされる
var task: Task? {
    //値がセットされた後に実行される
    //渡されたTaskインスタンスの値をラベルに反映
    didSet{
        if let t = task {
            taskLabel.text = t.text
                
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy/MM/dd"
            dedlineLabel.text = formatter.string(from: t.dedline)
        }else{
            return
        }
    }
}

ViewControllerクラス(Controller層)のtableView(_ cellForRowAt indexPath:)メソッド

tableView(_ cellForRowAt indexPath:)メソッドはテーブルのセルに表示するデータを指定します。
UITableViewDataSourceプロトコルのメソッドなのでViewControllerクラスにプロトコルを継承する必要があります。

またUITableViewDataSourceプロトコルを継承した場合、
tableView(_ cellForRowAt indexPath:)メソッドとtableView(_ numberOfRowsInSection section:)メソッドを実装する必要があります。

//ViewControllerクラス

//セルに表示するデータを設定
func tableView(_ tableView: UITableView,
               cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //セルを作成。viewDidLoad内で紐付けしたセル(TaskListCellクラス)を使用
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! TaskListCell

    //index番号に応じたデータ(Taskインスタンス)を取得
    let task = dataSource.indexData(indexPath.row)

    //セル(TaskListCell)のtaskプロパティにデータ(Taskインスタンス)をセット
    //TaskListCellのラベルにデータがセットされる
    cell.task = task

    return cell
}

tableView.dequeueReusableCell(withIdentifier:)メソッドでセルを生成し、生成したセルはTaskListCell(UITableViewCellを継承)にします。
※)ここで使用するセル(TaskListCellクラス)はViewControllerクラス内でtableViewと予め紐付けされています。

TaskDataSourceクラス(dataSource)のindexDataメソッドからセルに表示させるデータ(Taskインスタンス)を取得し、セル(TaskListCellクラス)のtaskプロパティ(プロパティオブザーバ)にデータをセットすることでセルにデータが表示(セット)されます。(厳密にはTaskListCellクラスのラベルにデータがセットされる)。

イニシャライザ

UITableViewCellクラスのイニシャライザのオーバーライドです。
各プロパティを初期化しています。

override init(style: UITableViewCellStyle, reuseIdentifier: String?){
    super.init(style: style, reuseIdentifier: reuseIdentifier)
        
    taskLabel = UILabel()
    taskLabel.font = UIFont.systemFont(ofSize: 14)
    //contentViewはセルを使って表示させるコンテンツのデフォルトのスーパービュー
    contentView.addSubview(taskLabel)
        
    dedlineLabel = UILabel()
    dedlineLabel.font = UIFont.systemFont(ofSize: 14)
    contentView.addSubview(dedlineLabel)
}

以下のページの説明が解りやすかったので参考にさせていただきました。

UIViewのサブクラス時にrequire init()が必要な場合の理由

Controller層

Controller層はModel層とView層の橋渡し役をします。
今回はModel層のTaskDataSourceクラスから保存されているデータを取得し、そのデータをView層のTaskListCellクラスに反映します。

ViewControllerクラス

今回はこのクラスがModel層とView層の橋渡し役をします。(プロジェクトを作成した時点で最初からあるクラスです)

※)新規でUIViewControllerを継承したクラスを作成でもどちらでもいいです。
この場合、作成したクラスをAppDelegateクラスのapplication(_:open:sourceApplication:annotation:)メソッドにてrootViewControllerに設定する必要があります。

import UIKit

class ViewController: UIViewController {
    //Model層のTaskDataSourceクラスからのデータを収めるプロパティ
    var dataSource: TaskDataSource!
    //セルを表示させるためのビュー
    var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        dataSource = TaskDataSource()
        
        tableView = UITableView(frame: view.bounds, style: .plain)
        //このクラスでtableViewのセルの設定を行います。
        tableView.delegate = self    //委譲先をselfに指定
        tableView.dataSource = self  //委譲先をselfに指定
        //tableViewとカスタムセル(TaskListCell)の紐付け
        tableView.register(TaskListCell.self, forCellReuseIdentifier: "Cell")
        view.addSubview(tableView)
        
        //ナビゲーションバーに設置するボタン(+の追加ボタン)
        let barBtn = UIBarButtonItem(barButtonSystemItem: .add,
                                        target: self,
                                        action: #selector(barBtnTap(_:)))
        //ナビゲーションバーにボタンを設置
        navigationItem.rightBarButtonItem = barBtn
        
    }
    
    //viewWillAppearはその画面を表示する度に呼ばれる
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        //画面が表示される度にデータを更新。
        //※getDataメソッドは配列tasksの内容を更新するメソッド
        dataSource.getData()
        //tableViewを更新する
        tableView.reloadData()
    }
    
    //ナビゲーションに追加したボタン(+の追加ボタン)をタップした時に呼ばれるメソッド
    //タスクの追加
    @objc func barBtnTap(_ sender: UIBarButtonItem){
        //タスクを作成する画面のController層のクラス
        let controller = CreateTaskViewController()
        //画面遷移
        present(controller, animated: true, completion: nil)
    }
    
    override func viewDidLayoutSubviews() {
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
}


//ViewControllerクラスでtableViewのセルの操作ができるようにプロトコルを実装します。
//このプロトコルメソッド内でmodel層の値をview層に渡したりします。
//こういった記述はTableViewControllerには必要ありません。
extension ViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView,
                   numberOfRowsInSection section: Int) -> Int {
        //セル(タスク)の数を返す
        return dataSource.count()
    }
    
    //セルに表示するデータを設定
    func tableView(_ tableView: UITableView,
                   cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        //セルを作成。viewDidLoad内で紐付けしたセルを使用
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! TaskListCell
        
        //index番号に応じたデータ(Taskインスタンス)を取得
        let task = dataSource.indexData(indexPath.row)
        //セル(TaskListCell)のプロパティにデータ(Taskインスタンス)をセット
        //TaskListCellのラベルにデータがセットされる
        cell.task = task
        
        return cell
    }
    
    //セルの高さを設定
    func tableView(_ tableView: UITableView,
                   heightForRowAt indexPath: IndexPath) -> CGFloat {
        tableView.estimatedRowHeight = 20 //セルの高さ
        return UITableViewAutomaticDimension //自動設定
    }
}

viewDidLoad内ではセルを表示させるUITableViewを生成し、そのtableViewとセルを紐付けています。
その他にはナビゲーションバーの設定と、tableViewのデリゲートの指定をしています。
UITableViewとカスタムのセル(TaskListCell)の関連付けには
register(_ cellClass: forCellReuseIdentifier identifier:)メソッドを使用しています。
詳しくはこちらをご覧ください。

viewWillAppear(_:)内ではタスクデータ(配列tasks)とtableViewの更新をしています。

ナビゲーションボタンをタップした際にbarBtnTap(_:)が呼ばれ「タスクを作成する画面」に遷移します。「タスクを作成する画面」は次で説明します。

tableViewより委譲されている処理はextension(拡張)で実装しています。

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

NEW TOPICS

/ ニュー & アップデート

SEE ALSO

/ 似た記事を見る

JY CONTENTS UNIQUE BLOG

search-menu search-menu