Help us understand the problem. What is going on with this article?

【Swift3】ToDoアプリを作る【CoreData】

More than 1 year has passed since last update.

はじめに

これを読んで身につく知識

  • 基本的なXcodeの使い方。
    • UIButtonやUITextFieldなどをストーリボードに追加し、それをクラスと連携させる方法。
  • CoreDataの基本的な使い方。
    • iOSアプリでは、SQLiteを用いてアプリ内のデータベースにデータを保存・検索などをすることができますが、CoreDataはそれをシンプルなコードで実現するフレームワークです。
    • 本記事を読めば、CoreDataを用いてデータの保存・検索・変更・削除ができるようになります。
  • UITableViewの効果的な使い方。
    • データベースに保存されているデータを効果的に表示する方法の1つとしてUITableViewがあります。
    • 本記事を読めば、データベースから持ってきたデータをUITableViewに表示するときに必要な知識が身につきます(データ追加・削除・変更時の挙動など)。

作ったもの・あなたが作れるようになるもの

iOSデベロッパーになる第一歩として、最初に作るべきは「ToDoアプリ」である。 - Steve Jobs

今回、iOSアプリ作成の練習のため、ToDoアプリを作成しました。
本記事では、そのアプリを作る過程をチュートリアルとしてまとめました。
これに従って作ればToDoアプリが出来上がるはずです(できなかったらコメントください)。
GitHubにコードを置いているので、これと比較しながら進めるとやりやすいかもしれません(本記事を最初に書いたときから結構アプリを改善したので、GitHubに置いているコードはこのチュートリアルで完成するコードとだいぶ違っていて参考にならないかもしれません)。
僕のようなiOSアプリ初心者の参考になれば嬉しいです。

今回作ったのはこんな感じのアプリです。
環境はSwift3/Xcode8で、タスクの保存にはCoreDataを用いています。
この程度のアプリならCoreDataを使う必要はないかもしれませんが、CoreDataを使ってはいけない理由もありませんからね。

CoreToDoDemo.gif

作り方

新しいプロジェクトを立ち上げる

File -> New -> Project... -> Single View Application

として、Product NameやOrganization Name、Organization Identifierは適当に決めます。
今回は「CoreToDo for Qiita」というProduct Nameにしました。
このとき、「Use Core Data」にチェックを入れるのを忘れないようにしてください。
これにチェックを入れると、CoreDataを使うに当たって必要なコードを自動的に生成してくれるので、チェックを入れ忘れると自分でそれを書く必要があるので面倒です(チェックせずにプロジェクトを始めたなら、作り直したほうが早いと思います)。

データベースモデルの作成

モデルの作成と言っても、「Use Core Data」にチェックを入れていればモデルは自動的に生成されています。
よって、そのモデルにエンティティと属性を追加すればOKです。

  • 「CoreToDo_for_Qiita.xcdatamodeld」を開き、「Add Entity」を押す。
  • エンティティの名前を「Task」にします。右側のData Model InspectorのClassのCodegenが「Class Definition」になっていることを確認してください。これはかなり重要です。
  • Attributesに「name」と「category」を追加します。Typeは両方「String」を選択してください。 entity.png

まずはMain.storyboardから

  • 右下のオブジェクトライブラリからTableView, Button(プラスマークの画像を使うと見た目が良いですね)を1つ目のビューに配置し、Constraintsを適当に設定します。
  • 同じく右下のオブジェクトライブラリからViewControllerをストーリーボードにドロップします。
  • そしてそのビューに先ほどと同様にLabel, TextField, Segmented Control, Button(×2 キャンセルボタンと追加ボタン)を配置し、Constraintsを設定します。
  • Segmented ControlのSegmentsを3にして、「"ToDo", "Shopping", "Assignment"」の3つを設定しておきます。
  • プラスボタンをCtrlを押しながらドラッグし、右側のビューにドロップ、Showを選択してsegueを繋ぎます。
  • オブジェクトを配置したら、色を自分好みに変更し、次の画像のようなビューになるようにしてください。 mainstoryboard.png

AddTaskViewControllerの追加とOutlet, Action接続

  • まずは先ほど追加した2つ目のビューに対応するViewControllerを追加します。

    File -> New -> File... -> Cocoa Touch Class
    

    を選択し、Subclass ofにUIViewControllerを選択し、Classを「AddTaskViewController」とします。
    AddTaskViewController.png

  • そして、ストーリーボードの右側のビューを選択し、ビューの上に出てくる3つアイコンのうち一番左のViewControllerアイコンを選択します。

  • Identity inspectorのCustom ClassのClassを先ほど作成した「AddTaskViewController」に設定します。

  • Main.storyboardを開き、左側のビューを選択します。そして、右上にあるオリンピックの出来損ないみたいなボタン(Assistant Editorといいます)を押します。

  • ビューのTableViewをCtrlを押しながら右側のエディタにドラッグ&ドロップし、Outlet接続します。名前はtaskTableViewとでもしておきましょう。

    ViewController.swift
    import UIKit
    
    class ViewController: UIViewController {
    
        // MARK: - Properties
    
        @IBOutlet weak var taskTableView: UITableView!
    
        // MARK: - View Life Cycle
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
    }
    
  • ちなみに、Xcodeでの開発では、上のファイルのように、// MARK: - Propertiesのようにコードをその役割で区切ることで見やすくするのが通例だそうです。初心者の皆さんはとりあえずマネしましょう。僕もマネしてます。

  • 次に、右側のビューを選択し、同じようにAssistant Editorを開き、同様にTextFieldとSegmentedControlをOutlet接続します。名前はtaskTextField、categorySegmentedControlとでもしておきましょう。

  • それと同様に、Segmented Controlとキャンセル用のボタン、追加用のボタンをAction接続します。メソッドの名前はcategoryChosen、cancelButtonTapped、plusButtonTappedとでもしておきましょう。

    AddTaskViewController.swift
    import UIKit
    
    class AddTaskViewController: UIViewController {
    
        // MARK: - Properties
    
        @IBOutlet weak var taskTextField: UITextField!
        @IBOutlet weak var categorySegmentedControl: UISegmentedControl!
    
        // MARK: - View Life Cycle
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
        // MARK: - Actions of Buttons
    
        @IBAction func categoryChosen(_ sender: Any) {
        }
    
        @IBAction func cancelButtonTapped(_ sender: Any) {
        }
    
        @IBAction func plusButtonTapped(_ sender: Any) {
        }
    
    }
    

    AddTaskViewController.swiftのコーディング

    まずはCoreDataを用いてタスクをデータベースに保存する機能を実装していきます。

    キャンセルボタン

    はじめに、1つ目のビューからプラスボタンで2つ目のビューを開いたあと、キャンセルボタンを押すと1つ目のビューに戻る機能を実装しましょう。cancelButtonTappedにdismissを記述するだけです。

    AddTaskViewController.swift
    import UIKit
    
    class AddTaskViewController: UIViewController {
    
        ...
    
        // MARK: - Actions of Buttons
    
        @IBAction func cancelButtonTapped(_ sender: Any) {
            dismiss(animated: true, completion: nil)
        }
    
        ...
    
    }
    
  • 上のコードを見れば分かるように、追加するコード以外は「...」で省略しています。これからもこのように省略していきますのでよろしくです。

  • それでは、この辺りで1度Runしてみて、プラスボタンとキャンセルボタンで1つ目と2つ目のビューを行き来できるかどうか試してみましょう(一度RunしないとCoreDataのモデルクラス(Task)が使用できない(多分)ので、その意味も込めて一度Runしておきましょう)。

    categoryChosenメソッド

    categorySegmentedControlが押されると、taskCategory変数に選択されたカテゴリーが代入されます。categoryChosenメソッドの引数を_ sender: Anyから_ sender: UISegmentedControlに変更するのを忘れないようにしてください。
    下のコードを見ても分かるように、確かにPropertiesだけど、上のOutletとはちょっと違う、みたいなときは// MARK: -として書く場所を少し分けることがよくあるみたいです。僕みたいな初心者はとりあえずマネしておきましょう。

    AddTaskViewController.swift
    import UIKit
    
    class AddTaskViewController: UIViewController {
    
        // MARK: - Properties
    
        @IBOutlet weak var taskTextField: UITextField!
        @IBOutlet weak var categorySegmentedControl: UISegmentedControl!
    
        // MARK: -
    
        var taskCategory = "ToDo"
    
        ...
    
        // MARK: - Actions of Buttons
    
        @IBAction func categoryChosen(_ sender: UISegmentedControl) {
            // choose category of task
            switch sender.selectedSegmentIndex {
            case 0:
                taskCategory = "ToDo"
            case 1:
                taskCategory = "Shopping"
            case 2:
                taskCategory = "Assignment"
            default:
                taskCategory = "ToDo"
            }
        }
    
        ...
    
    }
    

plusButtonTappedメソッド

プラスボタンが押されると、TextFieldに入力されている文字列とSegmentedControlで選択されているカテゴリーがCoreDataを介してデータベースに保存されます。コードの解説は下のコード中に書き込みました。基本は理解しているつもりですが、contextなどの詳しい仕様はまだまだ勉強が必要だと思います。

AddTaskViewController.swift
import UIKit

class AddTaskViewController: UIViewController {

    ...

    // MARK: - Actions of Buttons

    @IBAction func plusButtonTapped(_ sender: Any) {

        // TextFieldに何も入力されていない場合は何もせずに1つ目のビューへ戻ります。
        let taskName = taskTextField.text
        if taskName == "" {
            dismiss(animated: true, completion: nil)
            return
        }

        // context(データベースを扱うのに必要)を定義。
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

        // taskにTask(データベースのエンティティです)型オブジェクトを代入します。
        // このとき、Taskがサジェストされない(エラーになる)場合があります。
        // 詳しい原因はわかりませんが、Runするか、すべてのファイルを保存してXcodeを再起動すると直るので色々試してみてください。
        let task = Task(context: context)

        // 先ほど定義したTask型データのname、categoryプロパティに入力、選択したデータを代入します。
        task.name = taskName
        task.category = taskCategory

        // 上で作成したデータをデータベースに保存します。 
        (UIApplication.shared.delegate as! AppDelegate).saveContext()

        dismiss(animated: true, completion: nil)
    }

    ...

}

ViewController.swiftのコーディング

ここまででデータベースにタスクデータを保存する機能は実装できました。
ViewController.swiftでは、それらのデータをCoreDataから持ってきてTableViewに表示します。

必要なプロパティを用意、TableViewのDataSource、Delegateを設定

TableViewの構成に必要なプロトコルを追記し、TableViewを構成するのに使用するプロパティを定義します。これらの使い方は後述します。そしてviewDidLoadメソッドでtaskTableViewのdataSourceとdelegateをViewControllerに設定しておきます。また、この状態ではエラーになりますが、気にせず進んでください。

ViewController.swift
import UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    // MARK: - Properties

    @IBOutlet weak var taskTableView: UITableView!

    // MARK: - Properties for table view

    var tasks:[Task] = []
    var tasksToShow:[String:[String]] = ["ToDo":[], "Shopping":[], "Assignment":[]]
    let taskCategories:[String] = ["ToDo", "Shopping", "Assignment"]

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        taskTableView.dataSource = self
        taskTableView.delegate = self
    }

}

TableView構成に必要なメソッド

プロトコルに準じて必要なメソッドを実装します。また、カテゴリーをセクションとしてTableViewを構成するので、それに必要なメソッドも実装します。その際、先ほど定義したプロパティ(tasksToShow, taskCategories)を使用します。また、この状態でRunしても、taskTableViewにはセクションしか表示されません。

ViewController.swift
import UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    ...

    // MARK: - Table View Data Source

    // taskCategories[]に格納されている文字列がTableViewのセクションになる
    func numberOfSections(in tableView: UITableView) -> Int {
        return taskCategories.count
    }

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return taskCategories[section]
    }

    // tasksToShowにカテゴリー(tasksToShowのキーとなっている)ごとのnameが格納されている
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tasksToShow[taskCategories[section]]!.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()

        let sectionData = tasksToShow[taskCategories[indexPath.section]]
        let cellData = sectionData?[indexPath.row]

        cell.textLabel?.text = "\(cellData!)"

        return cell
    }

}

CoreDataからデータを持ってくる

いよいよCoreDataからデータを持ってきて(fetchする、といいます)、taskTableViewに表示するための配列に格納していきます。
ビューが表示される前にviewWillAppearというメソッドが自動的に実行されます。このメソッドをoverrideして、その中でデータをfetchし、taskTableViewに表示する配列に格納したあとに、taskTableViewを再読み込みします。詳しい解説は下のコード中に書き込んでいます。また、NSFetchRequestクラスはCoreDataをimportしないと使えないので、忘れずにimportしましょう。

ViewController.swift
import UIKit
import CoreData

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    ...

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        taskTableView.dataSource = self
        taskTableView.delegate = self
    }

    override func viewWillAppear(_ animated: Bool) {
        // CoreDataからデータをfetchしてくる
        getData()

        // taskTableViewを再読み込みする
        taskTableView.reloadData()
    }

    // MARK: - Method of Getting data from Core Data

    func getData() {
        // データ保存時と同様にcontextを定義
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        do {
            // CoreDataからデータをfetchしてtasksに格納
            let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
            tasks = try context.fetch(fetchRequest)

            // tasksToShow配列を空にする。(同じデータを複数表示しないため)
            for key in tasksToShow.keys {
                tasksToShow[key] = []
            }
            // 先ほどfetchしたデータをtasksToShow配列に格納する
            for task in tasks {
                tasksToShow[task.category!]?.append(task.name!)
            }
        } catch {
            print("Fetching Failed.")
        }
    }

    ...

}

Runして確認!

ここまでで、データベースに保存したデータがtaskTableViewに表示され、プラスボタンでデータを保存できるようになっているはずです。
Runしてデータの追加を試してみましょう!
うまく表示されなかったりするなら、何か間違っているので戻ってミスを探しましょう。

削除機能の実装

ここまでで、データを追加して一覧表示するところまで実装できました。
次は、taskTableViewで左方向にスワイプすれば削除ボタンが登場し、削除ができるようにしたいと思います。
とは言っても次のように1つのメソッドを追加するだけで実現できます。

ViewController.swift
import UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    ...

    // MARK: - Table View Data Source

    ...

    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

        if editingStyle == .delete {
            // 削除したいデータのみをfetchする
            // 削除したいデータのcategoryとnameを取得
            let deletedCategory = taskCategories[indexPath.section]
            let deletedName = tasksToShow[deletedCategory]?[indexPath.row]
            // 先ほど取得したcategoryとnameに合致するデータのみをfetchするようにfetchRequestを作成
            let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
            fetchRequest.predicate = NSPredicate(format: "name = %@ and category = %@", deletedName!, deletedCategory)
            // そのfetchRequestを満たすデータをfetchしてtask(配列だが要素を1種類しか持たない)に代入し、削除する
            do {
                let task = try context.fetch(fetchRequest)
                context.delete(task[0])
            } catch {
                print("Fetching Failed.")
            }

            // 削除したあとのデータを保存する
            (UIApplication.shared.delegate as! AppDelegate).saveContext()

            // 削除後の全データをfetchする
            getData()
        }
        // taskTableViewを再読み込みする
        taskTableView.reloadData()
    }

編集機能の実装

削除機能が実装できたので、次はtaskTableViewのセルをタップすると、そのデータの編集ができるようにしましょう。
編集機能を実装するには、今までに書いたコードを少しずつ変更していく必要があるので、少々面倒ですがミスに気をつけて進めていきましょう。

まず、セルのタップをビュー遷移と紐づける

  • 何もしなくてもTableViewのセルはタップできますが、タップでビュー遷移を行うには、少し手続きが必要です。まず、Main.storyboardを開き、taskTableViewに右下のライブラリからTableViewCellをドラッグ&ドロップします。そして、それによってできるPrototype Cellsの中に1つLabelを置きます。Constraintsは適当に設定しましょう。
  • そして、そのPrototype CellsのAttributes InspectorのIdentifier(placeholderにはReuse Identifierと書いてあります)に、「TaskCell」と忘れずに入力しておきましょう。大事です。

prototypecells.png

  • そしてFile -> New -> File... -> Cocoa Touch Classを選択し、UITableViewCellのサブクラスである「TaskTableViewCell」を作成します。 TaskTableViewCell.png
  • TaskTableViewCell.swiftを作成したなら、Prototype CellsのIdentity Inspectorへ行き、Custom ClassのClassをTaskTableViewCellに設定します。
  • 最後に、Prototype CellsからCtrlを押しながら2つ目のビューへドラッグ&ドロップし、showを選択してsegueを作ります。作成されたsegueをクリックし、Attributes InspectorのIdentifierを「SegueEditTaskViewController」に忘れずに設定しておきましょう。忘れると後で困ります。プラスボタンからのsegueと合わせてsegueが2つあるので、間違えないようにしましょう。別に両方に設定しても大丈夫です(多分)。

TaskTableViewCell.swiftのコーディング

先ほどPrototype CellsのIdentifierに設定した文字列と、セルにタスクを表示するためのLabelをプロパティとして定義します。忘れずにMain.storyboardのPrototype CellsとOutlet接続しておきましょう。コードのほうに先に@IBOutletを書いた場合は、Assistant EditorのほうからCtrlを押さずにストーリーボードのLabelにドラッグ&ドロップしてあげれば接続されます。

TaskTableViewCell.swift
import UIKit

class TaskTableViewCell: UITableViewCell {

    // MARK: - Properties

    static let reuseIdentifier = "TaskCell"

    // MARK: -

    @IBOutlet var taskLabel: UILabel!

    // MARK: - Initialization

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

}

taskTableViewのdataSourceの書き換え

taskTableViewのセルをTaskTableViewCellクラスに変更したため、それに応じてdataSourceのメソッドの1つを書き換える必要があります。書き換えるメソッドとどう書き換えるかは下のコードをご覧ください。これで今までどおりタスク一覧が表示されるはずです

ViewController.swift
import UIKit
import CoreData

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    ...

    // MARK: - Table View Data Source

    ...

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = taskTableView.dequeueReusableCell(withIdentifier: TaskTableViewCell.reuseIdentifier, for: indexPath) as? TaskTableViewCell else {
            fatalError("Unexpected Index Path")
        }

        let sectionData = tasksToShow[taskCategories[indexPath.section]]
        let cellData = sectionData?[indexPath.row]

        cell.taskLabel.text = "\(cellData!)"

        return cell
    }

    ...

}

segueで値を1つ目のビューから2つ目のビューへ渡す

これまでどおりタスク一覧が表示されていると思いますが、現状では、セルをタップしても普通のタスク追加画面に遷移するだけです。
セルをタップしてそのセルのデータを編集できる画面へ遷移するには、遷移先のビューへタップしたセルの情報をprepareメソッドを利用して渡してあげる必要があります。
それに当たり、渡す相手(AddTaskViewController.swift)に受け皿となる変数を用意してあげる必要があります。次のコードのように受け皿となる変数を用意し、segueで値をその受け皿に渡してあげましょう。

AddTaskViewController.swift
import UIKit

class AddTaskViewController: UIViewController {

    ...

    // MARK: - Properties

    var context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    var task: Task?

    ...

}
ViewController.swift
import UIKit
import CoreData

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    ...

    // MARK: - Properties

    private let segueEditTaskViewController = "SegueEditTaskViewController"

    ...

    // MARK: - Navigation

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        guard let destinationViewController = segue.destination as? AddTaskViewController else { return }

        // contextをAddTaskViewController.swiftのcontextへ渡す
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        destinationViewController.context = context
        if let indexPath = taskTableView.indexPathForSelectedRow, segue.identifier == segueEditTaskViewController {
            // 編集したいデータのcategoryとnameを取得
            let editedCategory = taskCategories[indexPath.section]
            let editedName = tasksToShow[editedCategory]?[indexPath.row]
            // 先ほど取得したcategoryとnameに合致するデータのみをfetchするようにfetchRequestを作成
            let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
            fetchRequest.predicate = NSPredicate(format: "name = %@ and category = %@", editedName!, editedCategory)
            // そのfetchRequestを満たすデータをfetchしてtask(配列だが要素を1種類しか持たないはず)に代入し、それを渡す
            do {
                let task = try context.fetch(fetchRequest)
                destinationViewController.task = task[0]
            } catch {
                print("Fetching Failed.")
            }
        }
    }

    ...

}

ViewControllerで受け取った値を初期値に設定する

受け皿として用意したtask変数に何か入っていれば、そのタスクのnameとcategoryをtextFieldとsegmentedControlに表示する必要があるので、その処理をviewDidLoadメソッドに記述します。

AddTaskViewController.swift
import UIKit

class AddTaskViewController: UIViewController {

    ...

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        // taskに値が代入されていたら、textFieldとsegmentedControlにそれを表示
        if let task = task {
            taskTextField.text = task.name
            taskCategory = task.category!
            switch task.category! {
            case "ToDo":
                categorySegmentedControl.selectedSegmentIndex = 0
            case "Shopping":
                categorySegmentedControl.selectedSegmentIndex = 1
            case "Assignment":
                categorySegmentedControl.selectedSegmentIndex = 2
            default:
                categorySegmentedControl.selectedSegmentIndex = 0
            }
        }
    }    

    ...

}

プラスボタン押下時の処理を修正する

受け取った値を初期値に設定することはできたが、この状態でプラスボタンを押下すると、同じデータが2つデータベースに保存されることになる。よって、plusButtonTappedメソッドの動作を修正する必要がある。具体的な修正法とその解説は下のコードに記述します。

AddTaskViewController.swift
import UIKit

class AddTaskViewController: UIViewController {

    ...

    // MARK: - Actions of Buttons

    ...

    @IBAction func plusButtonTapped(_ sender: Any) {

        let taskName = taskTextField.text
        if taskName == "" {
            dismiss(animated: true, completion: nil)
            return
        }

        // 受け取った値が空であれば、新しいTask型オブジェクトを作成する
        if task == nil {
            task = Task(context: context)
        }

        // 受け取ったオブジェクト、または、先ほど新しく作成したオブジェクトそのタスクのnameとcategoryに入力データを代入する
        if let task = task {
            task.name = taskName
            task.category = taskCategory
        }

        // 変更内容を保存する
        (UIApplication.shared.delegate as! AppDelegate).saveContext()

        dismiss(animated: true, completion: nil)
    }

}

完成です!

以上で機能面は完成となります。
タスク一覧の表示、プラスボタンでのタスク追加、スワイプでのタスク削除、セルタップによるタスク編集、すべての機能が実装できているはずです。
Runしてタスクを追加したり編集したり削除したりしてみましょう!

デザインをかわいくする

ここからはすべて僕の自己満足です。
機能面は完成したので、ToDoアプリとしては普通に使用することができます。
ですが、デザインが無機質すぎる、ということで少しいじってかわいくしようと思います。
とは言っても、taskTableViewのセクションの背景色や文字サイズ、色を変更したり、セルの文字色を変更したりするだけですけどね。。。

TableViewのセクションの見た目をいじる

TableViewのセクションの見た目は以下のようなメソッドを記述することで変更できます。
1つ目のメソッドでセクションの文字色、文字サイズ、背景色、配置(中央寄せなど)、フォントを変更することができます。2つ目のメソッドで、セクションの高さを変更することができます。

ViewController.swift
import UIKit
import CoreData

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    ...

    // MARK: - Table View Data Source

    ...

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let title = UILabel()

        title.text = taskCategories[section]

        title.textAlignment = NSTextAlignment.center
        title.backgroundColor = UIColor(red: 255/255, green: 215/255, blue: 0/255, alpha: 1.0)
        title.textColor = .brown
        title.font = UIFont(name: "Helvetica Neue", size: 20.0)

        return title
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 40.0
    }

    ...

}

セルの文字色を変更

すでに記述している以下のメソッドに、セルの文字色を茶色に変更するコードを書いてみましょう。
これで完成です。最初の動画そのままのアプリが出来上がりました。

ViewController.swift
import UIKit
import CoreData

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    ...

    // MARK: - Table View Data Source

    ...

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = taskTableView.dequeueReusableCell(withIdentifier: TaskTableViewCell.reuseIdentifier, for: indexPath) as? TaskTableViewCell else {
            fatalError("Unexpected Index Path")
        }

        let sectionData = tasksToShow[taskCategories[indexPath.section]]
        let cellData = sectionData?[indexPath.row]

        cell.taskLabel.text = "\(cellData!)"
        cell.taskLabel.textColor = .brown    // ここを追加しただけです。

        return cell
    }

    ...

}

今度こそ完成です!

Runして遊んでみましょう!

おわりに

今回このToDoアプリを作ってみて、Swift3/Xcode8の基本がしっかり身についたことに加え、CoreDataの基本的な使い方も身についたと思います。
ToDo, Shopping, Assignment以外のカテゴリーを追加できる機能や、カテゴリ内での並び替え機能など、まだまだ追加するべき機能はあると思いますが、とりあえずはToDoアプリ完成ということで!

次は、今回身につけた技術を応用し、家計簿アプリのようなものを作りたいと思っています。
また、外部との通信を行う機能(APIを使ったり、Facebookログイン機能であったり)を実装するにはまた高い壁がありそうで楽しみです。
家計簿を作るとなると、グラフ表示などに苦戦すると思いますが、完成したときにはまたQiitaに投稿したいと思います。

ちなみに、最初に書いた名言はSteve Jobsのものではありません。
僕の名言です。

最後までお読みいただき、ありがとうございました。

改良(GitHub参照)

  • 2017年1月29日
    • カテゴリー一覧を保持するオブジェクトをtaskCategories[]のみにし、それをグローバル化しました。
  • 2017年1月30日
    • 画面の回転を禁止、縦方向での使用のみに制限しました。
  • 2017年1月31日
    • ユーザが新しいカテゴリを追加できる機能を実装しました。
    • ユーザがカテゴリを削除できる機能を追加しました(デフォルトの3つは削除できません)。
    • 1つ目のSegmentedControlに表示しているカテゴリを5個に設定し、それ以降のカテゴリは2つ目のSegmentedControlに表示するようにしました。
    • キーボードがDoneボタンで隠れるようにしました。タスク編集時はカテゴリを追加できないようにしました。
  • 2017年2月11日
    • 細かい機能を修正しました。
  • 2017年2月24日
    • カテゴリ選択ボタン(UISegmentedControl)を3段にしました。

これから実装したい機能

  • カテゴリ内での並び替え機能
  • リマインド機能
  • 長いメモをタスクの名前とは別に保存する機能
  • 重要なタスクを目立たせる機能
snowman_mh
つよいエンジニアです
https://snowman-mh.github.io
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした