[iOSアプリ開発] Todoリスト表示アプリ:Realm利用

この記事は、データ永続化CoreDataを利用した前作の続編になります。前作で作成したTodoリストアプリのデータ永続化にRealmを利用します。

さらに、UIを改良するために、SwipeCellの導入(スワイプで不要な項目を削除)、Chameleoフレームワークの導入(リストにグラデーションをつけて見やすくする)、ナビゲーションバーの配色の設定などしていきます。

目次

概要

この講座で学習する内容
  • Todoリスト表示アプリ作成

アプリを作成する過程で、以下の内容を学習できます。

  • Realmを利用したデータの永続化
  • SwipeCellの使い方(スワイプで不要な項目を削除)
  • Chameleonフレームワークの使い方(リストにグラデーションをつけて見やすくする)
  • ナビゲーションバーの配色の設定

MVCデザインパターンを適用しています。
Model(M)、View(V)、Controller(C
記事中、M、V、Cの略を使用しています。

参考にした講座】iOS & Swift – The Complete iOS App Development Bootcamp (Udemy)
 (Section19) Local Data Persistance – User Defaults, Core Data and Realm

この講座で作成するアプリの概要
  1. このアプリのデータはRealmを利用して永続化をしていますので、アプリを終了、クラッシュしてもデータが消えることはありません。
  2. カテゴリー画面は、カテゴリーのリストが表示されています。右上のプラスボタンでカテゴリーを追加することができます。
  3. あるカテゴリーを選択すると、次画面(Todoリスト画面)に遷移します。
  4. Todoリスト画面は、選択したカテゴリーのリストを表示します。
  5. 右上のプラスボタンでリストを追加することができます。
  6. 上部にあるSearchBarを使用して特定のリストを抽出することができます。
アプリの開発環境(2023年7月16日現在)
  • Xcode: Version 14.3.1
  • macOS: Venture: Version 13.4.1
  • iOS: 16.5.1
Xcodeのインストールが未了の方はこちら
GitHubからプロジェクトを入手したい方はこちら

Realmのインストールから初期設定まで

STEP
Realmインストール情報の入手

Realmのトップページから、下図のように選択します。

「Swift SDK」を選択します。

「Install Realm」→「Cocoa Pods」を選択します。

STEP
Realmのインストールを手順書に従って進めます。「Pod init」の実行

ターミナルでTodoアプリが保存されているフォルダに移動し、「Pod init」を実行します。

STEP
Podファイルの修正

開いたPodファイルを以下の通り修正します。

STEP
「Pod install」の実行

ターミナルに戻り、「Pod install」を実行します。

STEP
ワークスペースファイルを開く

ターミナルを閉じ、Finderに戻りワークスペースファイルを開きます。

STEP
AppDelegateにコード追加

Realmデータベースのインスタンスを初期化するための処理コードを追加しています。

Realm:データ保存

Dataモデルの作成

CoreDataで作成したDataモデルをRealmで利用できるように設定します。

STEP
Itemクラスの作成

Swiftファイルを新規作成し、「Item」クラスを作成します。

import Foundation
import RealmSwift

class Item: Object {
    @objc dynamic var title: String = ""
    @objc dynamic var done: Bool = false
    var parentCategory = LinkingObjects(
        fromType: Category.self, 
        property: "items")
}
  1. import RealmSwiftは、RealmSwiftフレームワークをプロジェクトに取り込むためのインポート文です。
  2. Itemクラスは、RealmSwiftのObjectクラスを継承しています。
    Objectクラスは、Realmデータベースのオブジェクトモデルを表すための基本クラスです。
  3. titledoneは、Itemクラスのプロパティです。
    それぞれ文字列型のStringと整数型のBoolであり、@objc dynamicという属性が指定されています。
@objc dynamicが適用されたプロパティ

以下のような特性を持ちます。

  1. ランタイムでの動的なアクセス:
    実行時にプロパティの値を読み書きすることが可能です。
  2. KVO(Key-Value Observing)のサポート:
    プロパティの値の変更を監視し、変更があった場合に通知を受け取ることができます
  3. Objective-Cのコンポーネントとの互換性:
    Objective-C側からプロパティにアクセスできるようになります。
ItemクラスとCategoryクラスのリレーションシップ

Itemクラス:

  • Itemクラスは、特定のカテゴリに属するアイテムを表します。
  • parentCategoryプロパティは、Itemオブジェクトが所属する親カテゴリを示します。
  • LinkingObjects(fromType:property:)コンストラクタは、指定したタイプのオブジェクトCategory.self)と、指定したプロパティ"items")の間の関連を表現します。
  • このリレーションシップにより、特定のカテゴリに関連付けられたアイテムをparentCategoryを介して取得することができます。
STEP
Categoryクラスの作成

Swiftファイルを新規作成し、「Category」クラスを作成します。

import Foundation
import RealmSwift

class Category: Object {
    @objc dynamic var name: String = ""
    let items = List<Item>()
}
  • Categoryクラスも、RealmのObjectクラスを継承しています。
  • nameは、Categoryクラスのプロパティであり、文字列型のStringです。@objc dynamicが付与されており、Realmがデータベースのカラムとして認識し、データの保存と取得を行うことができます。
ItemクラスとCategoryクラスのリレーションシップ

Categoryクラス:

  • Categoryクラスは、アイテムのカテゴリを表します。
  • itemsプロパティは、List<Item>型であり、Itemオブジェクトのリストを保持します。
  • ListはRealmSwiftが提供するコンテナ型であり、複数のアイテムを格納するためのものです。
  • このリレーションシップにより、特定のカテゴリに関連付けられたアイテムのリストをitemsプロパティを介して取得することができます。

要するに、ItemクラスとCategoryクラスのリレーションシップは、1つのカテゴリに複数のアイテムが所属する関係を表しています。ItemクラスのparentCategoryプロパティを使用してアイテムがどのカテゴリに属するかを示し、Categoryクラスのitemsプロパティを使用してカテゴリに関連付けられたアイテムのリストを取得することができます。これにより、アイテムとカテゴリの間の関連性を簡単に表現し、データの取得や操作を行うことができます。

Realm:データ保存

Realmを使用してデータを保存するコードを記載します。

1.class CategoryViewController(抜粋)

class CategoryViewController: UITableViewController {

//解説1:    
    let realm = try! Realm()
    
    var categories = [Category]()
    
    //MARK: - Data Manipulation Methods
//解説2:
    func save(category: Category) {
        do {
            try realm.write {
                realm.add(category)
            }
        } catch {
            print("Error saving context \(error)")
        }
        tableView.reloadData()
    }

    
    //MARK: - Add New Categories
    @IBAction func addButtonPressed(_ sender: UIBarButtonItem) {
        var textField = UITextField()
        
        let alert = UIAlertController(
        title: "Add New Category", 
        message: "", 
        preferredStyle: .alert)
        let action = UIAlertAction(
        title: "Add", 
        style: .default) { (action) in

            //Addボタンが押された時の処理
            let newCategory = Category()
            newCategory.name = textField.text!
            
            self.categories.append(newCategory)
            
            self.save(category: newCategory)
            
        }
        alert.addAction(action)
    }
}
解説1:let realm = try! Realm()
let realm = try! Realm()
  • Realm()を使用してデータベースのインスタンスを作成します。
  • try!は、エラーが発生した場合にアプリケーションをクラッシュさせる強制的なエラー無視を行います。これにより、Realmデータベースのインスタンスを安全に取得します。
解説2:func save(category: Category)
func save(category: Category) {
    do {
        try realm.write {
            realm.add(category)
        }
    } catch {
        print("Error saving context \(error)")
    }
    tableView.reloadData()
}

オブジェクトを保存するためのメソッドが定義されています。このメソッドは、カテゴリを受け取り、それをRealmデータベースに保存します。

  • realm.writeブロック内でカテゴリの保存を行っています。
    realm.writeは、Realmデータベースのトランザクションを開始するためのメソッドです。
    トランザクション内でデータの変更を行うことができます。
  • realm.add(category)は、Categoryオブジェクトをデータベースに追加するためのメソッドです。これにより、指定したカテゴリがRealmデータベースに保存されます。
  • エラーが発生した場合は、catchブロック内でエラーメッセージを表示します。
  • tableView.reloadData()は、データの保存後にテーブルビューを再読み込みし、更新されたデータを表示するためのメソッドです。

Realm:データ読み取り

Realmを使用してデータを読み取るコードを記載します。

1.class CategoryViewController(抜粋)

import UIKit
import RealmSwift

class CategoryViewController: UITableViewController {
    
    let realm = try! Realm()

//解説1:    
    var categories: Results<Category>?
    
    override func viewDidLoad() {
        super.viewDidLoad()

        loadCategories()
    }

//解説2:
    //MARK: - TableView Datasource Methods
    override func tableView(_ tableView: UITableView, 
        numberOfRowsInSection section: Int) -> Int {
        return categories?.count ?? 1
    }

//解説3:    
    override func tableView(_ tableView: UITableView, 
        cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: 
            "CategoryCell", for: indexPath)
        
        cell.textLabel?.text = categories?[indexPath.row].name 
            ?? "No Categories Added"
        
        return cell
    }
    
    //MARK: - TableView Delegate Methods
    override func tableView(_ tableView: UITableView, 
        didSelectRowAt indexPath: IndexPath) {
        performSegue(withIdentifier: "goToitems", sender: self)
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let destinationVC = segue.destination as! TodoListViewController
        
        if let indexPath = tableView.indexPathForSelectedRow {
            destinationVC.selectedCategory = categories?[indexPath.row]
        }
    }
    
    //MARK: - Data Manipulation Methods
    func loadCategories() {
//解説4:
        categories = realm.objects(Category.self)

        tableView.reloadData()
    }
}
解説1:var categories: Results<Category>?
var categories: Results<Category>?
  • categoriesは、Results<Category>型のオプショナル変数です。
  • Resultsは、RealmSwiftのデータ型であり、クエリの結果として返されるデータのコレクションです。
  • Categoryは、カテゴリを表すオブジェクトの型です。
解説2:tableView(_:numberOfRowsInSection:)
override func tableView(_ tableView: UITableView, 
    numberOfRowsInSection section: Int) -> Int {
    return categories?.count ?? 1
}

categories?.count ?? 1は、カテゴリの数があればその数を、なければ1を返します

解説3:tableView(_:cellForRowAt:)の解説
override func tableView(_ tableView: UITableView, 
    cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: 
        "CategoryCell", for: indexPath)
        
    cell.textLabel?.text = categories?[indexPath.row].name 
        ?? "No Categories Added"
        
    return cell
}

categories?[indexPath.row].name ?? "No Categories Added"は、指定されたインデックスパスのカテゴリの名前をセルのテキストに設定します。カテゴリが存在しない場合は、「No Categories Added」と表示します。

解説4:categories = realm.objects(Category.self)の解説
categories = realm.objects(Category.self)
  • RealmデータベースからCategoryオブジェクトの全てのインスタンス(カテゴリー)を取得するためのメソッドです。

2.class TodoListViewController(抜粋)

import UIKit
import RealmSwift

class TodoListViewController: UITableViewController {

//解説1:   
    var todoItems: Results<Item>?
    let realm = try! Realm()
    
    var selectedCategory : Category? {
        didSet {
            loadItems()
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        loadItems()
    }
    
    //MARK - TableView Datasource Methods
    
    override func tableView(_ tableView: UITableView, 
        numberOfRowsInSection section: Int) -> Int {
        return todoItems?.count ?? 1
    }
    
    override func tableView(_ tableView: UITableView, 
        cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: 
            "ToDoItemCell", for: indexPath)
        
        if let item = todoItems?[indexPath.row] {
            cell.textLabel?.text = item.title
            
            cell.accessoryType = item.done ? .checkmark : .none
        } else {
            cell.textLabel?.text = "No Items Added"
        }
        return cell
    }
    
//MARK -Add New Items
    
    @IBAction func addButtonPressed(_ sender: UIBarButtonItem) {
        
        var textField = UITextField()
        
        let alert = UIAlertController(title: "Add New Todoey Item", 
            message: "", preferredStyle: .alert)
        let action = UIAlertAction(title: "Add Item", style: .default) { (action) in
            //Addボタンが押された時の処理
            
            if let currentCategory = self.selectedCategory {
                do {
                    try self.realm.write {
                        let newItem = Item()
                        newItem.title = textField.text!
                        currentCategory.items.append(newItem)
                    }
                } catch {
                    print("Error saving new items, \(error)")
                }
            }
            self.tableView.reloadData()
        }
    }
    
    func loadItems() {
//解説2:        
        todoItems = selectedCategory?.items
        .sorted(byKeyPath: "title", ascending: true)

        tableView.reloadData()
    }
}
解説1:var todoItems: Results?
var todoItems: Results<Item>?
  • Resultsは、クエリの結果として得られるデータのコレクションです。
  • Itemは、Todoリストの項目を表すオブジェクトの型です。
  • Results<Item>型は、データベースから取得したItemオブジェクトを保持するための変数です。
解説2:todoItems = selectedCategory?.items...
todoItems = selectedCategory?.items
    .sorted(byKeyPath: "title", ascending: true)

このコードは、selectedCategoryに関連付けられたTodoリストの項目を読み込んで並び替えるための処理を行っています。以下に解説します。

  • selectedCategory: カテゴリオブジェクトを表す変数です。選択されたカテゴリを保持しています。
  • items: カテゴリに関連付けられたTodoリストの項目を表すプロパティです。このプロパティは、Categoryクラス内で定義されています。
  • sorted(byKeyPath:ascending:): 結果セットの並び替えを行うためのメソッドです。第1引数で指定したキーパス(ここでは”title”)を基準に、項目を昇順(ascending: true)に並び替えます。

Realm:データ更新

Realmを使用してデータを更新するコードを記載します。

1.class TodoListViewController(抜粋)

import UIKit
import RealmSwift

class TodoListViewController: UITableViewController {
    
    var todoItems: Results<Item>?
    let realm = try! Realm()
       
//MARK - TableView Delegate Methods
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        if let item = todoItems?[indexPath.row] {
            do {
                try realm.write {
                    item.done = !item.done
                }
            } catch {
                print("Error saving done status, \(error)")
            }
        }
        tableView.reloadData()
    }
}
tableView(_:didSelectRowAt:)の解説
  • tableView(_:didSelectRowAt:): テーブルビューのセルが選択されたときに呼び出されるデリゲートメソッドです。indexPathは、選択されたセルの位置を示すインデックスパスです。
  • if let item = todoItems?[indexPath.row] では、todoItemsから選択されたセルに対応する Item オブジェクトを取得します。オプショナルバインディングを使用して、todoItemsnil でない場合にのみ処理が実行されます。
  • try realm.write ブロック内で、Realm データベースのトランザクションを開始し、選択された項目の done プロパティ(完了状態)を反転させます。
  • エラーが発生した場合、catch ブロックが実行され、エラーメッセージが表示されます。
  • 最後に、tableView.reloadData() が呼び出されて、テーブルビューの再読み込みが行われます。これにより、項目の完了状態の変更がテーブルビューに反映されます。

Realm:データ削除

Realmを使用してデータを削除するコードを記載します。

1.class TodoListViewController(抜粋)

//MARK - TableView Delegate Methods
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        if let item = todoItems?[indexPath.row] {
            do {
                try realm.write {
                    realm.delete(item)
                }
            } catch {
                print("Error saving done status, \(error)")
            }
        }
        tableView.reloadData()
    }
tableView(_:didSelectRowAt:)の解説
  • try realm.write ブロック内で、Realm データベースのトランザクションを開始し、選択された項目を削除します。具体的には、realm.delete(item) を使用して item を削除しています。

Realm:データのクエリ

Realmを使用してデータをクエリするコードを記載します。

1.class TodoListViewController(抜粋)

//MARK: - Search bar methods

extension TodoListViewController: UISearchBarDelegate {

    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {

        todoItems = todoItems?.filter("title CONTAINS[cd] %@", searchBar.text!)
            .sorted(byKeyPath: "title", ascending: true)
        
        tableView.reloadData()
    }

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        if searchBar.text?.count == 0 {
            loadItems()

            DispatchQueue.main.async {
                searchBar.resignFirstResponder()
            }
        }
    }
}
todoItems = todoItems?.filter(“title CONTAINS[cd] %@”, searchBar.text!)
.sorted(byKeyPath: “title”, ascending: true)
の解説
  • todoItems: フィルタリングの対象となる項目のリストを保持している変数です。
  • filter(_:args:)メソッドを使用して、todoItemsをフィルタリングしています。
  • title CONTAINS[cd] %@は、フィルタリング条件を指定しています。ここで、titleItemオブジェクトのtitleプロパティを表し、CONTAINS[cd]titleが指定された文字列を部分的に(大文字・小文字を区別せずに)含むことを示します。%@はフィルタリング条件にマッチするテキストを表します。
  • searchBar.text!は、検索バーのテキストを取得し、フィルタリング条件に指定しています。
  • sorted(byKeyPath:ascending:)メソッドを使用して、フィルタリング後の結果をタイトルに基づいて昇順に並び替えます。

UIの改良

SwipeCellの導入→不要データ削除

不要になったカテゴリーを削除できるようにするため、CocoaPodsからSwipeCellを導入します。

STEP
CocoaPodsからSwipeCellKitを検索
https://cocoapods.org/pods/SwipeCellKit
STEP
SwipeCellKitのインストール

1.Xcodeを終了し、ターミナルを起動し、Todoeyアプリが格納されているフォルダへ移動します。

2.次にPodファイルを開きます。

3.Podファイルに「pod ‘SwipeCellKit’」を追記

4.ターミナルで「pod install」を実行

STEP
Xcodeワークスペースファイルを開き、cellForRoeATの修正

CocoaPodsのDocumentationを参考にして、XcodeのcellForRoeATを修正(追記)する。

https://cocoapods.org/pods/SwipeCellKit
STEP
SwipeTableViewCellDelegateプロトコルの追加

CocoaPodsのDocumentationを参考にして、SwipeTableViewCellDelegateプロトコルを追加します。

https://cocoapods.org/pods/SwipeCellKit

このコードは、CategoryViewControllerクラスがSwipeTableViewCellDelegateプロトコルに準拠しており、セルのスワイプ操作に関する処理を提供しています。以下に解説します。

  • tableView(_:editActionsForRowAt:for:)メソッド:
    セルのスワイプアクションを設定するためのデリゲートメソッドです。
  • tableViewは対象のテーブルビュー、indexPathはスワイプされたセルの位置、orientationはスワイプの方向を表します。
  • guard orientation == .right else { return nil }は、スワイプの方向が右でない場合は何も返さずに処理を終了します。
  • SwipeAction(style:title:handler:)で、削除アクションを作成します。.destructiveスタイルは削除を意味し、"Delete"が表示されるタイトルです。handlerクロージャでは、削除操作に対応するモデルの更新などを行います。
  • deleteAction.imageで、アクションの外観をカスタマイズできます。例では、”delete-icon”という名前の画像を指定しています。
  • 最後に、配列として作成したアクションを返します。
STEP
「delete-icon」画像の入手

githubのページに行き、「Example」→「Mail Example」→「Assets.xcassets」→「Trash.imageset」からTrashicon.pngを入手します。

https://github.com/SwipeCellKit/SwipeCellKit/tree/develop/Example/MailExample/Assets.xcassets/Trash.imageset
STEP
Custom Classの「Class」と「Module」を変更する
Class:SwipeTableViewCell
Module:SwipeCellKit
STEP
セルの高さ調整

アプリを実行すると、セルの高さが低く「delete-icon」が隠れてしまうので、セルの高さを調整します。

調整前
調整後
STEP
削除ボタンを押した時に、削除するコードを追記

スワイプで削除できるようにする

今は削除ボタンを押して削除していますが、メールアプリのようにドラッグだけで削除できるようにします。

https://cocoapods.org/pods/SwipeCellKit

CocoaPodsのDocumentationを参考にして、コードを追記しました。

CategoryViewControllerに作成したSwipeのエクステンションを新クラスに移行

2つのViewControllerが、スワイプで削除を継承できるSuperClassを作成します。

STEP
CocoaTouchクラスの作成
STEP
2つのViewのセルIdentifierを共通の名前に変更します、
STEP
コードの作成(SwipeTableViewController)
import UIKit
import SwipeCellKit

class SwipeTableViewController: UITableViewController, SwipeTableViewCellDelegate {
    
    override func viewDidLoad() {
        super.viewDidLoad()       
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! SwipeTableViewCell
              
        cell.delegate = self
        
        return cell
    }
    
    func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? {
        guard orientation == .right else { return nil }

        let deleteAction = SwipeAction(style: .destructive, title: "Delete") { action, indexPath in
            // handle action by updating model with deletion
            
            self.updateModel(at: indexPath)
        }

        // customize the action appearance
        deleteAction.image = UIImage(named: "delete-icon")

        return [deleteAction]
    }
    
    func tableView(_ tableView: UITableView, editActionsOptionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions {
        var options = SwipeOptions()
        options.expansionStyle = .destructive

        return options
    }
    
    func updateModel(at indexPath: IndexPath) {
        // Update our data model
    }
}
func updateModel(at indexPath: IndexPath)の解説

updateModel(at:)メソッドは、データモデルの更新を行うためのメソッドです。具体的には、指定されたindexPathに基づいてデータモデルの更新処理を実装します。

STEP
コードの作成(CategoryViewController)
import UIKit
import RealmSwift

class CategoryViewController: SwipeTableViewController {
    
    let realm = try! Realm()
    
    var categories: Results<Category>?

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = super.tableView(tableView, cellForRowAt: indexPath)
        
        cell.textLabel?.text = categories?[indexPath.row].name ?? "No Categories Added"
        
        return cell
    }
    
    //MARK: - Delete Data From Swipe
    
    override func updateModel(at indexPath: IndexPath) {
        if let categoryForDelection = self.categories?[indexPath.row] {
            do {
                try self.realm.write {
                    self.realm.delete(categoryForDelection)
                }
            } catch {
                print("Error detecting category, \(error)")
            }
        }
    }
}
tableView(_:cellForRowAt:)の解説

このコードは、CategoryViewControllerクラスでtableView(_:cellForRowAt:)メソッドをオーバーライドしています。このメソッドは、テーブルビューの各行(セル)を表示する際に呼び出されます。

  • super.tableView(tableView, cellForRowAt: indexPath): 親クラスであるSwipeTableViewControllertableView(_:cellForRowAt:)メソッドを呼び出して、基本のセルを取得します。
  • cell.textLabel?.text = categories?[indexPath.row].name ?? "No Categories Added": 取得したセルのtextLabelプロパティに、表示するテキストを設定します。
  • categories?[indexPath.row].nameは、categoriesプロパティから該当する位置のCategoryオブジェクトのnameプロパティを取得します。ただし、categoriesが存在しない場合や指定した位置にオブジェクトがない場合は、デフォルトのテキストとして”No Categories Added”が表示されます。
  • 最後に、更新したセルを返します。
override func updateModel(at indexPath: IndexPath)の解説

updateModel(at:)メソッドは、スワイプアクションなどによってデータモデルを更新するためのメソッドです。このメソッドは、SwipeTableViewControllerクラスで定義されており、CategoryViewControllerクラスでオーバーライドされています。

CategoryViewControllerクラスのupdateModel(at:)メソッドの具体的な実装は次のとおりです:

  • if let categoryForDelection = self.categories?[indexPath.row]: categoriesプロパティから指定された位置のCategoryオブジェクトを取得します。このオブジェクトが存在する場合にのみ、削除の処理を実行します。
  • try self.realm.write:
    Realmのトランザクションを開始します。トランザクション内で行われる変更は、データベースに反映されます。
  • self.realm.delete(categoryForDelection):
    取得したCategoryオブジェクトをRealmデータベースから削除します。

TodoListViewControllerにおいてもスワイプで削除できるようにする

TodoListViewControllerにおいてもスワイプで削除できるようにコードを追記します。

class TodoListViewController: SwipeTableViewController {
    
    var todoItems: Results<Item>?
    let realm = try! Realm()
        
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = super.tableView(tableView, cellForRowAt: indexPath)
        
        if let item = todoItems?[indexPath.row] {
            cell.textLabel?.text = item.title
            
            cell.accessoryType = item.done ? .checkmark : .none
        } else {
            cell.textLabel?.text = "No Items Added"
        }
        return cell
    }
       
    //MARK: - Model Manupulation Methods

    override func updateModel(at indexPath: IndexPath) {
        if let item = self.todoItems?[indexPath.row] {
            do {
                try self.realm.write {
                    self.realm.delete(item)
                }
            } catch {
                print("Error detecting category, \(error)")
            }
        }
    }
}
tableView(_:cellForRowAt:)の解説

SwipeTableViewControllerクラスで定義されているtableView(_:cellForRowAt:)メソッドをオーバーライドしています。

  • super.tableView(tableView, cellForRowAt: indexPath)で、親クラスのtableView(_:cellForRowAt:)メソッドを呼び出し、基本のセルを取得します。
  • if let item = todoItems?[indexPath.row]で、指定された位置のItemオブジェクトを取得します。このオブジェクトが存在する場合には、セルのテキストをitem.titleに設定します。
  • cell.accessoryTypeを使って、item.donetrueの場合はチェックマークを、それ以外の場合はチェックマークを表示しないように設定します。
  • オブジェクトが存在しない場合(todoItemsnilの場合)は、セルのテキストを”No Items Added”に設定します。
updateModel(at:)の解説

SwipeTableViewControllerクラスで定義されているupdateModel(at:)メソッドをオーバーライドしています。

  • if let item = self.todoItems?[indexPath.row]で、削除対象のItemオブジェクトを取得します。
  • try self.realm.writeブロック内で、Realmトランザクションを開始し、取得したItemオブジェクトを削除します。

Chameleonフレームワークの導入

Chameleonフレームワークを導入して、ランダムな色を生成し、テーブルビューの各セルに色を効率的につけていきます。

https://github.com/wowansm/Chameleon/tree/swift5#readme
STEP
Podファイルに追記
STEP
ターミナルから「pod install」を実行
STEP
ランダム色のコードを追記
https://github.com/vicc/chameleon#uicolor-methods
tableView.separatorStyle = .none

tableViewseparatorStyleプロパティに.noneを設定しています。これにより、テーブルビューのセル間のセパレータ(区切り線)が非表示になります。セル同士が区切られず、一体感のある表示を実現することができます。

cell.backgroundColor = UIColor.randomFlat()

セルの背景色をランダムに設定しています。UIColor.randomFlat()は、Flat UIカラーパレットからランダムな色を生成するメソッドです。これにより、各セルが異なる色で表示されることで、視覚的な区別が生まれます。

STEP
アプリを再起動した後も色が保存されるようにコードを追記
class CategoryViewController: SwipeTableViewController {
        
    //MARK: - TableView Datasource Methods
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = super.tableView(tableView, cellForRowAt: indexPath)
        
        
        cell.textLabel?.text = 
        categories?[indexPath.row].name ?? "No Categories Added"
        
        cell.backgroundColor = 
        UIColor(hexString: categories?[indexPath.row].colour ?? "1D9BF6")
        
        return cell
    }
   
    //MARK: - Add New Categories
    @IBAction func addButtonPressed(_ sender: UIBarButtonItem) {
        var textField = UITextField()
        
        let alert = UIAlertController(title: "Add New Category", message: "", preferredStyle: .alert)
        let action = UIAlertAction(title: "Add", style: .default) { (action) in
            //Addボタンが押された時の処理
            //Core Dataのコンテキストを使用して新しい項目を作成
            let newCategory = Category()
            newCategory.name = textField.text!
            newCategory.colour = UIColor.randomFlat().hexValue()
            
            self.save(category: newCategory)
            
        }
    }
}
UIColor(hexString: categories?[indexPath.row].colour ?? “1D9BF6”)

指定された位置のCategoryオブジェクトのcolourプロパティを使用してセルの背景色を設定します。ただし、categoriesが存在しない場合や指定した位置にオブジェクトがない場合は、デフォルトのカラーコード”1D9BF6“が使用されます。

newCategory.colour = UIColor.randomFlat().hexValue()
  • UIColor.randomFlat(): UIColorクラスのrandomFlat()メソッドを使用して、ランダムなフラットデザインの色を生成します。このメソッドは、Flat UIカラーパレットからランダムな色を返します。
  • hexValue(): UIColorオブジェクトのhexValue()メソッドを使用して、カラーコードを取得します。このメソッドは、色を16進数の文字列として表現します。
STEP
各セルの色をグラデーションにする
  • FlatSkyBlue(): FlatSkyBlueという色を表すUIColorオブジェクトを作成します。これは、Flat UIカラーパレットの中から青色を表しています。
  • darken(byPercentage: CGFloat(indexPath.row) / CGFloat(todoItems!.count)): FlatSkyBlue()に対してdarken(byPercentage:)メソッドを呼び出します。これにより、元の色を指定した割合だけ暗くした新しい色が生成されます。割合はインデックスパスの行番号をカテゴリ内の総アイテム数で割った値です。行番号が上がるごとに割合が増え、より暗い色が生成されます。
  • if let colour = ...: darken(byPercentage:)メソッドの結果をcolourという名前の定数に代入します。もし結果が存在する(darken(byPercentage:)が有効な色を返した場合)、if let文の中の処理が実行されます。
  • cell.backgroundColor = colour: セルの背景色を生成された色に設定します。これにより、セルが行ごとに徐々に暗くなる効果が得られます。
STEP
各セルの色に応じて文字の色を白黒反転させる

背景色をグラデーションにすることができましたが、色が濃くなると文字が読めなくなるため、文字が読めるようにコードを追記します。

https://github.com/vicc/chameleon#contrasting-colors
STEP
カテゴリーの色を基準にTodoリストの色を決める

アプリをユーザが見やすくなるような設定をしていきます。

STEP
ナビゲーションバーの文字を大きく
STEP
ナビゲーションバーの色を選択したカテゴリーの色にする
func viewWillAppear

ビューコントローラの画面が表示される直前に呼び出されるviewWillAppearメソッドの中で実行される処理です。

  • if let colourHex = selectedCategory?.colour: selectedCategoryオブジェクトのcolourプロパティからカテゴリのカラーコードを取得します。selectedCategoryが存在する場合は、カラーコードがcolourHexとして代入されます。
  • guard let navBar = navigationController?.navigationBar else { ... }: ナビゲーションコントローラが存在するかを確認します。もしナビゲーションコントローラが存在しない場合、エラーメッセージを表示してアプリケーションをクラッシュさせます。
  • navBar.scrollEdgeAppearance?.backgroundColor = UIColor(hexString: colourHex): ナビゲーションバーのスクロールエッジの外観(アピアランス)に対して、背景色を設定します。背景色には、カラーコードを使ってUIColorオブジェクトを生成しています。これにより、ナビゲーションバーの外観がカテゴリの色に設定されます。
STEP
Todoリストビューのタイトルに選択したカテゴリー名を表示する
STEP
検索バー周辺の色を選択したカテゴリーの色にする
STEP
ナビゲーションバーの背景色、文字色を選択したカテゴリーの色により調整する

このコードは、ビューコントローラが表示される直前に、ナビゲーションバーの外観をカスタマイズしています。具体的には、selectedCategoryオブジェクトの色情報を使用してナビゲーションバーの背景色、テキストの色、およびタイトルを設定しています。

  • theColourWeAreUsing: 取得した色情報を使用して、ナビゲーションバーの背景色として使用する色オブジェクトを作成します。
  • contrastCoulour: theColourWeAreUsingの対比色を取得します。
  • navBarAppearance: ナビゲーションバーの外観を設定するためのオブジェクトを作成します。
NavigationBarの色設定
  • navBarAppearance.largeTitleTextAttributes: ナビゲーションバーの大見出し(Large Title)のテキストの属性を設定します。ここでは、テキストの前景色(.foregroundColor)をcontrastCoulourに設定しています。大見出しは、通常、ナビゲーションバーの上部に表示される大きなテキストです。
  • navBarAppearance.backgroundColor: ナビゲーションバーの背景色をtheColourWeAreUsingに設定します。つまり、カテゴリに関連付けられた色がナビゲーションバーの背景色として使用されます。
  • navBar?.tintColor: ナビゲーションバーのアイテム(ボタンやテキストなど)の色をcontrastCoulourに設定します。これにより、アイテムがナビゲーションバーの背景色と対比して視認性が向上します。
  • navBar?.scrollEdgeAppearance: スクロールしたときのナビゲーションバーの外観をnavBarAppearanceに設定します。この設定により、スクロールした際にも一貫した外観が保たれます。
STEP
Categoryビューのナビゲーションバーの色、カテゴリータイトルの色を変更
viewWillAppear

ビューコントローラの表示が画面に表示される直前に呼び出されるviewWillAppearメソッドのオーバーライドです。このメソッド内でナビゲーションバーの外観を設定しています。

  • guard let navBar = navigationController?.navigationBar: ナビゲーションコントローラが存在するかを確認し、存在する場合はnavigationBarnavBarという定数に代入します。ナビゲーションコントローラが存在しない場合、アプリケーションは異常終了(fatalError)します。
  • navBar.scrollEdgeAppearance?.backgroundColor: ナビゲーションバーのスクロールエッジ(上部)の外観を設定します。ここでは、背景色を「1D9BF6」に設定しています。
cell.textLabel?.textColor = ContrastColorOf(categoryColour, returnFlat: true)
  • UIColor(hexString: category.colour): カテゴリの色情報(category.colour)を元に、カラーオブジェクトを作成します。hexStringパラメータは16進数のカラーコードを受け取り、それを基にカラーオブジェクトを作成します。カラーオブジェクトが正常に作成できない場合、アプリケーションは異常終了(fatalError)します。
  • cell.backgroundColor = categoryColour: セルの背景色をカテゴリの色に設定します。
  • cell.textLabel?.textColor = ContrastColorOf(categoryColour, returnFlat: true): セルのテキストの色を、背景色に対してのコントラストカラーに設定します。ContrastColorOfメソッドは、指定された背景色に対してコントラストのあるテキストカラーを取得します。

この記事が気に入ったら
いいね または フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次