LoginSignup
0
1

[Swift]Realmのデータを更新(削除)する時に配列にするかResults構造体かで少し悩んだ話

Posted at

初めに

SwiftでRealmを触っているときにデータを配列で受け取るかResult<>で受け取るかについて少し悩んでしまったので記事にしようと思いました。

どんなアプリを作ろうとしてたか

・シンプルなTodoアプリ
・画面は2つ
・保存するデータはタスクと誰がやるかの2つ
・1つの画面でTextFieldで追加したTodoが、もう1つの画面でTableView上で一覧で表示される。
・データの管理はRealmで行う。

データは配列で受け取る形で実装してみた

ソースコードは以下の通りです。今回の記事では一旦エラーハンドリングは無視して行います。

データ構造
class TodoList: Object {
    @Persisted var task: String = ""
    @Persisted var member: String = ""
}
データ追加画面
import UIKit
import RealmSwift

class NewTodoViewController: UIViewController {
    @IBOutlet var taskTextField: UITextField!
    @IBOutlet var memberTextField: UITextField!
    let realm = try! Realm()

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    @IBAction func save() {
        let todo = TodoList()
        todo.task = taskTextField.text ?? ""
        todo.member = memberTextField.text ?? ""
        createTodo(todo)
    }
    func createTodo(todo: TodoList) {
        try! realm.write{
            realm.add(todo)
        }
    }
}
一覧表示画面
import UIKit
import RealmSwift

class ViewController: UIViewController{
    @IBOutlet var tableView: UITableView!

    var todos: [TodoList] = []
    let realm = try! Realm()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        todos = readTodos()

    }
    override func viewWillAppear(_ animated: Bool) {
        items = readTodos()
        tableView.reloadData()
    }
    
    func readTodos() -> [TodoList]{
        return Array(realm.objects(TodoList.self)
    }
}

extension ViewController: UITableViewDataSource {
    //numberOfRowsInSectionとcellForRowAtをよしなに書くだけなので省略。
}

ざっくり上記のようなTodoListを作りました。
その後、TodoListの削除機能を作ろうと思いました。要はTableViewを横にスライドしたらセルを削除したいということです。
そこで色々調べているうちに、どうやらextensitonの中に以下のコードを参考に書けばいいということがわかりました。

削除するための参考
import UIKit
import RealmSwift
class ViewController: UIViewController {
    //省略
    var textList : Results<TextList>!
    //省略
}
extension ViewController: UITableViewDataSource {
    //numberOfRowsInSectionとcellForRowAtをよしなに書く
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
            if editingStyle == UITableViewCell.EditingStyle.delete {
              try! realm.write {
                realm.delete(textList[indexPath.row])
              }
                tableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.fade)
            }
        }
}

これで削除できるとあったので、extensionの中の関数をそのままコピペして実行してみました。
そこで実行してみたところ、アプリがクラッシュしました。
エラーメッセージは以下の通りです。

Thread 1: "Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). Table view: ; backgroundColor = ; layer = ; contentOffset: {0, 0}; contentSize: {414, 45}; adjustedContentInset: {0, 0, 34, 0}; dataSource: >

訳してみると、どうやらtableviewで表示するデータの数が整合性が取れなくなっており、一致しなくなってしまっているとのこと。

なるほど確かにコードをみてみるとrealm.delete(textList[indexPath.row])をしてRealm内のデータを削除しているだけで、データを実際に管理している配列Todosは更新されていませんでした。そのため削除前の要素数をxとすると削除後にtableViewの要素数を取得する時に呼ばれる関数numberOfRowsInSectionで返ってくる値は要素の数がx-1ではなくxのままで(データを実際に管理している配列Todosが更新されていないため)、プログラム的にはeditingStyleを呼び出した時点で要素数がx-1になってる!と想定しているのに実際にxがnumberOfRowsInSectionから返ってきたため一致せず、整合性が取れないことでエラーが起きているということです。
そこで上記のコードに、以下のようにデータを実際に管理している配列Todosを更新(今回の場合は削除)するためのコードを書き足しました。

書き足した後の一覧画面
import UIKit
import RealmSwift

class ViewController: UIViewController{
    @IBOutlet var tableView: UITableView!

    var todos: [TodoList] = []
    let realm = try! Realm()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        todos = readTodos()

    }
    override func viewWillAppear(_ animated: Bool) {
        todos = readTodos()
        tableView.reloadData()
    }
    
    func readTodos() -> [TodoList]{
        return Array(realm.objects(TodoList.self)
    }
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return todos.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "SampleCell", for: indexPath)
        let todo: TodoList = todos[indexPath.row]
        cell.textLabel!.text = todo.task
        return cell
    }
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        try! realm.write {
            realm.delete(self.todos[indexPath.row])
        }
        //この一行を追加
        todos = readItems()
        tableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.fade)
    }
}

上記のように、Realmからコードを削除した後にitems配列を再度Realmから取得し更新を行なってからtableViewのセルを削除しました。ちなみに今回は再度Realmから配列を取得しましたが、Realmから取得せず、配列から選択したセルの要素を削除するコードだけでも実装可能です。
そうすると問題なくアプリを動かすことができ、無事削除機能は実装できました。

さてここまでプログラムはタイトルにもある通り、配列でデータを操作してきました。
ですが配列だと、Realm内のデータと繋がっていないため少し面倒だなと感じるところがあります。例えば先ほどの削除機能の実装の際も、Realm内のデータの更新をわざわざ配列に反映させる必要がありました
そんな問題を解決するために今度はデータをResults構造体でRealmから受け取るように実装してみます。Results構造体でRealmからデータを受け取った時は、データのコピーではなくデータそのものを受け取るため更新がそのままデータベース(今回はRealm)の更新になります。

データはResults構造体で受け取る形で実装してみた

import UIKit
import RealmSwift

class ViewController: UIViewController {
    @IBOutlet var tableView: UITableView!
    var todos: Results<TodoList>!
    let realm = try! Realm()
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        todos = readTodos()

    }
    override func viewWillAppear(_ animated: Bool) {
        todos = readTodos()
        tableView.reloadData()
    }
    
    func readTodos() -> Results<TodoList>?{
        return realm.objects(TodoList.self)
    }


}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return todos.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! ItemTableViewCell
        let todo: ShoppingItem = totos[indexPath.row]
        cell.textLabel!.text = todo.task

        return cell
    }
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        try! realm.write {
            realm.delete(self.todos[indexPath.row])
        }
        tableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.fade)
    }
}

データ追加画面とデータ構造は全く同じコードで大丈夫です。
上記のコードでは、データの受け取り方をResults構造体で実装してみました。
そうすると削除の部分などは、コード的にはRealm内のデータしか削除されていませんが今回はデータをResults構造体itemsで受け取っているため、Realm内のデータとitemsが繋がっており片方を更新すれば自動でもう一方も更新されるのです。

結局どっちを使うべきなんだろうって話

先ほどまでに示したようにどちらの方法でも問題なく実装できます。
一応メリットとデメリットを比較してみました。
配列を使うメリット
・配列のためシンプルに操作できる
配列を使うデメリット
・データが静的である(更新が同期されない)

Results構造体を使うメリット
・データが動的である(更新が同期される)
Results構造体を使うデメリット
・配列では使えてた一部のメソッドが使えなくなる

どちらもメリットとデメリットが存在するので個人的には
リアルタイムなデータの変更を反映させたいときやLazy loadingが重要なときはResults構造体
通常の配列メソッドや型安全な操作が必要な場合はArray
でいいかなと思いました。
ここまで読んでいただきありがとうございました。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1