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プロトコルを実装する方法があります。
今回はプロトコルを実装する方法としない方法を記述します。
プロトコルを実装しない方法
独自クラスのインスタンスを別の型(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
}
}
この方法についての詳細は以下の記事をご覧ください。
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()
}
}
出力
カウント: 2
打ち合わせ: 2019-04-19 16:06:43 +0000
食事: 2019-04-19 16:06:43 +0000
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)
}
}
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(拡張)で実装しています。