MILLEN BOX 2

個人iOSアプリ開発者hollymotoによる勉強の記録。時々雑記。

RealmでTabelViewControllerを使ったリスト表示アプリを作った

この記事で書いたリスト表示アプリをRealmで書き直したり、自分が流用しやすいように改造してみました。

f:id:anthrgrnwrld:20200416123832g:plain

以下の記事の続編みたいな感じです。

www.millenbox2.com

目次

1. 変更ポイント

1-1. 仕様

実際に使うことを考えたらこうした方がいいかなってとこを変更しました。(変更してからこれを書いてるんで漏れあるかもしれませんが...)

  • Addボタンを押した時にリストに追加するのはなくDeatilViewControllerに遷移させる。(リストへの追加はDeatilViewControllerからListViewControllerに戻るタイミングで行うことにします。後述します。)
  • DetailViewControllerへの遷移で、Addボタンを押して遷移したか、それともTableVIewCellをタップして遷移したかを判別できるようにする。
  • Addボタンを押してDetailViewControllerへ遷移した時に、DeatilViewControllerにタップした日時のテキストを渡すようにする。(DeatilViewController側の対応も必要。後述します。)
  • TableVIewCellをタップしてDetailViewControllerへ遷移した時に、どのCellのタップから遷移したがわかるようにindexPath情報をDeatilViewControllerへ渡すようにする。(DeatilViewController側の対応も必要。後述します。)

1-2. Realm関連

Realm対応を行う際に注意すべきことは以下です。 * リストの並び替えに対応する場合にはRealmオブジェクトの順番を保持するList型の配列で管理しなければならない(Realmオブジェクトの順番が登録順になるという保証がない為)。 * ListViewController及びDeatilViewControllerの表示処理の際には直接Realmデータベースに保存してある情報を参照する。 * Addボタンを押してDetailViewControllerへ遷移からListViewControllerに戻るタイミングでRealmデータベースにオブジェクトを追加する。

2. ソースコード

2-1. TapDateObject

Addボタンをタップした日時等を管理するRealmオブジェクトクラスです。
1.1でもあげましたが、List型のオブジェクトクラスも追加します。

TapDateObject.swift

import Foundation
import RealmSwift

class TapDateObject: Object {
    @objc dynamic var id: Int = 0               //id(主キーにするプロパティ)
    @objc dynamic var tapDate: Date!            //タップした日時
    @objc dynamic var objDescription: String?   //表示文字列
    
    convenience init(date: Date) {
        self.init()
        tapDate = date
        objDescription = date.description
    }
    
    override static func primaryKey() -> String? {
        return "id"
    }

}

class TapDateList: Object {
    let list = List<TapDateObject>()
}

2-2. RealmPrimaryKeyIncrementerProtocol

2-1で記載しましたTapDateObjectの中にidというプロパティがあります。これはPrimaryKeyを管理するためのものです。ここに入るInt型の数値は固有のものでなければなりません。そのようにする為にRealmPrimaryKeyIncrementerProtocolを準備しました。以下の記事をそのまま参考にさせて頂きました。

qiita.com

RealmPrimaryKeyIncrementerProtocol.swift

import RealmSwift

protocol RealmPrimaryKeyIncrementerProtocol {
    func newId<T: Object>(model: T) -> Int
}

extension RealmPrimaryKeyIncrementerProtocol {

    func newId<T: Object>(model: T) -> Int {
        guard let key = T.primaryKey() else { fatalError("このオブジェクトにはプライマリキーがありません") }

        // Realmのインスタンスを取得
        let realm = try! Realm()
        // 最後のプライマリーキーを取得
        if let last = realm.objects(T.self).sorted(byKeyPath: "id", ascending: true).last,
            let lastId = last[key] as? Int {
            return lastId + 1 // 最後のプライマリキーに+1した数値を返す
        } else {
            return 0  // 初めて使う際は0を返す
        }
    }
}

2-3. ViewController

変更無しですが一応以下に貼っときます。
(ただしStoryBoardに遷移用のボタンを追加しています。詳しくはこの記事を見てみて下さい。)

import UIKit

class ViewController: UIViewController {

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

}

2-3. ListViewController

変更点はDetailViewControllerへの遷移時の動作や引き渡すデータの違いとRealmデータベースからの表示情報の取得。元のコードをコメントアウトしたまま残していますのでご参考に。

ListViewController.swift

import UIKit
import RealmSwift

// MARK: - ListViewController
class ListViewController: UITableViewController, RealmPrimaryKeyIncrementerProtocol {
    
    //var objects = [Any]()
    var lastTapDate: NSDate?        //最後にAddをタップしたDateを保存しとく
    var tappedIndexPath: IndexPath? //DetailViewControllerからの情報の引き継ぎ
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //self.clearsSelectionOnViewWillAppear = false  //TableViewでの現在の選択を解除するか否か
        setNavigationItem()         //Navigation BarのボタンItemをセットする
    }
    
    
    // MARK: - extension ListViewController for Public Method
    
    /**
     Detail View Controllerから戻る時に実行する(ViewControllerのTableViewCellをタップして遷移から戻ってきた場合)
    */
    func returnFromAnotherVC(_ updatedIndexPath: Any) {
        tableView.reloadData()
    }
    
    /**
     Detail View Controllerから戻る時に実行する(AddボタンでこのVCに遷移から戻ってきた場合)
    */
    func performToInsertAction(_ updatedIndexPath: Any) {
        if let indexPath = updatedIndexPath as? IndexPath {
            tableView.insertRows(at: [indexPath], with: .automatic)
        }
    }
    
}

// MARK: - extension ListViewController for Private Method
extension ListViewController {
    
    /**
     Navigation BarのボタンItemをセットする
    */
    private func setNavigationItem() {
        
        //Navigation Bar 左側
        //Editボタン追加
        self.navigationItem.leftBarButtonItem = self.editButtonItem
        
        
        //Navigation Bar 左側
        //Addボタン追加
        let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(pressAddButton(_:)))
        navigationItem.rightBarButtonItem = addButton
    }
}

// MARK: - extension ListViewController for selector
extension ListViewController {
    
    @objc
    func pressAddButton(_ sender: Any) {
        lastTapDate = NSDate()
        shiftToAnotherVC(lastTapDate!)
    }

    func shiftToAnotherVC(_ tapDate: Any) {
        self.performSegue(withIdentifier: "showDetail", sender: tapDate)
    }
    
}



// MARK: - extension ListViewController delegate
extension ListViewController {
    
    // MARK: - Table view data source

    //ListのSection数を返す
    override func numberOfSections(in tableView: UITableView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        return 2
    }
    
    //Sectionのタイトル(※このメソッドを書かない場合にはSection帯が表示されない)
    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        let sectionTitle: String = "\(section) section"
        return sectionTitle
    }

    //TableViewのセル数(Section毎)
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        if section != 0 {
            return 0;
        }
        
        var cellCount = 0;
        //通常
        //cellCount = objects.count
        
        //Realm
        let realm = try! Realm()
        let tapDateList = realm.objects(TapDateList.self)
        if tapDateList.first == nil {
            cellCount = 0
        } else {
            cellCount = tapDateList.first!.list.count
        }
        return cellCount
    }
    
    //各セルの要素を設定する
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if indexPath.section != 0 {
            fatalError("section\(indexPath.section) is invalid")
        }
        
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        
        //通常
        //let object = objects[indexPath.row] as! NSDate
        //cell.textLabel!.text = object.description
        
        //Realm
        let realm = try! Realm()
        let tapDateList = realm.objects(TapDateList.self)
        let dateDescription = tapDateList.first!.list[indexPath.row].objDescription
        cell.textLabel!.text = dateDescription
        
        return cell
    }
    
    //Editボタンが押されると呼ばれる (※このメソッドを書かない場合にはEditボタンを押すと自動的に編集モードになる)
    override func setEditing(_ editing: Bool, animated: Bool) {
        super.setEditing(editing, animated: animated)
        tableView.isEditing = editing   //編集モードに設定
    }

    //削除可能なセルのindexPathを指定(※このメソッドを書かない場合には全てのCellが削除可能)
    override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        // Return false if you do not want the specified item to be editable.
        return true     //すべてのCellを削除可能
    }

    //削除された時の処理を実装する
    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            // Delete the row from the data source
            //通常
            //objects.remove(at: indexPath.row)                   //先にデータの更新
            
            //realm
            let realm = try! Realm()
            let tapDateList = realm.objects(TapDateList.self)
            let targetItem = tapDateList.first!.list[indexPath.row]
            try! realm.write {
                realm.delete(targetItem)
            }
            
            tableView.deleteRows(at: [indexPath], with: .fade)
            //それからテーブルの更新 (この順番を間違えると落ちる)
        } else if editingStyle == .insert {
            // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
        }
    }


    //並び替え可能なセルの indexPath を指定(※このメソッドを書かない場合には全てのCellが並び替え不可能)
    override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        return true    //すべてのCellを並び替え可能にする
    }
    
    //リストの並び替え
    override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) {
        //データの順番を整える
        //通常
        //let targetItemObject = objects[fromIndexPath.row]
        //objects.remove(at: fromIndexPath.row)
        //objects.insert(targetItemObject, at: to.row)
        
        //realm
        let realm = try! Realm()
        let tapDateList = realm.objects(TapDateList.self)
        let targetItemRealm = tapDateList.first!.list[fromIndexPath.row]
        let targetItemRealmCopy = TapDateObject(date: targetItemRealm.tapDate)
        targetItemRealmCopy.id = newId(model: targetItemRealmCopy)
        try! realm.write {
            realm.delete(targetItemRealm)
            tapDateList.first!.list.insert(targetItemRealmCopy, at: to.row)
        }
    }
    
}

// MARK: - extension ListViewController Navigation
extension ListViewController {
    
    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destination.
        // Pass the selected object to the new view controller.
        if segue.identifier == "showDetail"{
            
            let controller = segue.destination as! DetailViewController
            
            if let tapDate = sender as? NSDate {
                //Addボタンにて遷移した時
                controller.tappedIndexPath = nil    //遷移方法の判別で使用する
                controller.newDateTmp = tapDate
            } else if let indexPath = tableView.indexPathForSelectedRow {
                //TableViewCellをタップして遷移した時
                controller.tappedIndexPath = indexPath    //遷移方法の判別で使用する
            } else {
                fatalError("IndexPath is inconvenience.")
            }
            
        }
    }
}

2-3. DetailViewController

変更点はListViewControllerからの遷移時に引き渡されるデータの処理と、内容確定時(= ListViewControllerへの戻り時)のRealmデータベースへの登録処理。

DetailViewController.swift

import UIKit
import RealmSwift

class DetailViewController: UIViewController, RealmPrimaryKeyIncrementerProtocol {

    @IBOutlet weak var detailDescriptionLabel: UILabel!
    
    var tappedIndexPath: IndexPath? //遷移元のTableViewでタップされたindexPathを記録する
    var newDateTmp: NSDate?         //新規で追加予定のしたTapDateの日時
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configureView()
        navigationController?.delegate = self   //遷移(From/In)のフックの為にdelegateをセット
    }
}

// MARK: - extension DetailViewController for Private Method
extension DetailViewController {
    
    func configureView() {
        
        if tappedIndexPath == nil {
            //AddボタンでこのVCに遷移してきた場合
            
            //遷移前に別Controllerでセットされた時にはnewDateTmpがnullの為、
            //viewDidLoadにて configureView()を実行する必要がある
            if let description = newDateTmp?.description {
                detailDescriptionLabel.text = description
            }
            
        } else {
            //リストのTableViewCellをタップして遷移してきた場合
            let realm = try! Realm()
            let tapDateList = realm.objects(TapDateList.self)
            detailDescriptionLabel.text = tapDateList.first!.list[tappedIndexPath!.row].objDescription
        }
        
    }
    
}

// MARK: - extension DetailViewController: UINavigationControllerDelegate
extension DetailViewController: UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
        
        if let controller = viewController as? ListViewController {   //遷移先がListViewController(=BACK時のみに絞っている)
            var updatedIndexPath = IndexPath(row: 0, section: 0)
            if tappedIndexPath != nil {
                //ListViewControllerのTableViewCellをタップして遷移してきた場合
                updatedIndexPath = tappedIndexPath!
                controller.returnFromAnotherVC(updatedIndexPath)
            } else {
                //AddボタンでこのVCに遷移してきた場合
                //Realm
                let realm = try! Realm()
                if let newDate = newDateTmp {   //AddボタンでこのVCに遷移してきた → newDateTmpがnilでない
                    let tapDate = TapDateObject(date: newDate as Date)  //Realm用オブジェクトを生成1
                    tapDate.id = newId(model: tapDate)                  //IDを割り当て
                    let tapDateList = realm.objects(TapDateList.self)
                    
                    try! realm.write {
                        if tapDateList.first == nil {
                            let tmpTapDateList = TapDateList()
                            tmpTapDateList.list.append(tapDate)
                            realm.add(tmpTapDateList)
                        } else {
                            tapDateList.first!.list.insert(tapDate, at: 0)
                        }
                    }
                    
                }
                controller.performToInsertAction(updatedIndexPath)  //updatedIndexPathは初期値のまま(row: 0, section: 0)でOK
            }
            
        }
    }
}

3. まとめ

リストを使ったアプリはデータを保存したいことが多いと思うので、データベースを使った内容保存の基本型を自分で持っておくのは有用だと感じました。引き続き精進します。