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

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

## どんなアプリを作ろうとしてたか
・シンプルなTodoアプリ
・画面は2つ
・保存するデータはタスクと誰がやるかの2つ
・1つの画面でTextFieldで追加したTodoが、もう1つの画面でTableView上で一覧で表示される。
・データの管理はRealmで行う。
## データは配列で受け取る形で実装してみた
ソースコードは以下の通りです。今回の記事では一旦エラーハンドリングは無視して行います。
```swift:データ構造
class TodoList: Object {
    @Persisted var task: String = ""
    @Persisted var member: String = ""
}
```
```swift:データ追加画面
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)
        }
    }
}
```
```swift:一覧表示画面
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の中に以下のコードを参考に書けばいいということがわかりました。
```swift:削除するための参考
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: <UITableView: 0x107042400; frame = (0 88; 414 808); clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x600000c73b70>; backgroundColor = <UIDynamicSystemColor: 0x600001779000; name = tableBackgroundColor>; layer = <CALayer: 0x6000004438e0>; contentOffset: {0, 0}; contentSize: {414, 45}; adjustedContentInset: {0, 0, 34, 0}; dataSource: <text_Realm.ViewController: 0x106c09410>>

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

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

```swift:書き足した後の一覧画面
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構造体で受け取る形で実装してみた
```swift:
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**
でいいかなと思いました。
ここまで読んでいただきありがとうございました。