RealmでTabelViewControllerを使ったリスト表示アプリを作った
この記事で書いたリスト表示アプリをRealmで書き直したり、自分が流用しやすいように改造してみました。
以下の記事の続編みたいな感じです。
目次
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を準備しました。以下の記事をそのまま参考にさせて頂きました。
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. まとめ
リストを使ったアプリはデータを保存したいことが多いと思うので、データベースを使った内容保存の基本型を自分で持っておくのは有用だと感じました。引き続き精進します。