Realmの「逆方向の関連」を利用してTodoリストを作る

TableViewの勉強を進めていると、Sectionが複数になった途端コードが散らかってしまいませんか？
２次元配列にしたり、Sectionクラスを作ってプロパティとしてrow要素を組み込んだり方法は色々ありますが、Realm の逆方向の関連を使うと比較的綺麗にまとまったので紹介します。

Sectionとrowだけでなく、一対多のネスト構造のもの全般に活躍できそうです。例えば、メインオブジェクトはゲームソフトだけど、新規作成はゲームの記録をメインとしたUIを想定している場合でも、Realmの逆方向の関連を活用すればUIに沿った形でコードが作成できるかと思います。

今回は、Realmの逆方向の関連を用いて、簡単なTodoアプリの新規作成機能を作りたいと思います。

**環境**　
Swift 5.2
     Xcode 11.4
     Realm 10.4.0

Realmのインストールが済んだ状態から進めます。

```swift:todo.swift

import FonDation
import RealmSwift

class Section: Object {
    @objc dynamic var name: String = ""
    //Taskオブジェクトとの逆方向の関連を明示
    let tasks = LinkingObjects(fromType: Task.self, property: "section")
}

class Task: Object {
    @objc dynamic var section: Section?
    @objc dynamic var name: String = ""
}
```
sectionに格納されるSectionクラスと、rowに格納されるTaskクラスは、一対多の関係なので、SectionクラスのプロパティにTaskをネストさせるのが自然です。が、どちらかというとSectionよりもTaskをメインオブジェクトとして扱いたいのと、後々検索やソートをかける時などTaskにSectionの情報が直接入っている方が便利な場合も多いため、TaskのプロパティにSectionを入れています。
新しく作ったTaskのsectionプロパティが既存のSectionオブジェクトと一致すると、自動でその配下に入る仕組みにより、SectionクラスとTaskクラスの一対多のネスト関係が成り立っています。これが逆方向の関連の強みの一つだと思います。

新しく作ったTaskオブジェクトのsectionプロパティが既存のSectionオブジェクトと一致した場合に、自動でその配下へ追加される仕組みです。ViewControllerは例えばこのような感じになります。


```swift:viewController.swift
import UIKit
import RealmSwift

class ViewController: UIViewController {

    
    @IBOutlet weak var tableView: UITableView!
    
    //既存のSectionを間違いなく選択できるようにする
    var pickerView: UIPickerView = UIPickerView()
    //テーブルビューの元の配列
    var list: Results<Section>?
    //Realmオブジェクト作成
    let realm = try!Realm()
    
    //タスク名を入れるテキストフィールド
    @IBOutlet weak var taskTextField: UITextField!
    
    //セクション名を入れるテキストフィールド
    @IBOutlet weak var SectionTextField: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //Sectionオブジェクトを全て呼び出しlist配列に格納
        list = realm.objects(Section.self)
        
        setUpTable()
        callPickerView()
    }
    
    //追加ボタン
    @IBAction func addButton(_ sender: Any) {
        
        do {
            try realm.write{
                //セクションのテキストフィールドの文字を元にRealmから呼び出す
                let section = realm.objects(Section.self).filter("name = '\(SectionTextField.text ?? "")'").first
                //タスクの作成、呼び出したsectionを格納、なければ新しくインスタンス作成
                let task = Task(value: ["name": taskTextField.text ?? "不明",
                                    "section": section ?? Section(value: ["name": SectionTextField.text ?? "不明"])])
                //新規タスクを追加
                realm.add(task)
            }
        }catch {
            
        }
        tableView.reloadData()
    }


}

extension ViewController {
    
    //tableViewのセットアップ
    func setUpTable(){
        tableView.delegate = self
        tableView.dataSource = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "taskCell")
        tableView.reloadData()
    }
    
    //SectionのテキストフィールドをタップするとpickerViewが呼び出される
    func callPickerView() {
        pickerView.delegate = self
        pickerView.dataSource = self
        let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 50))
        let item1 = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done))
        let flexibleItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
        toolbar.setItems([item1, flexibleItem], animated: true)
        
        SectionTextField.inputView = pickerView
        SectionTextField.inputAccessoryView = toolbar
    }
    
    //PickerViewの完了ボタンをタップした時の処理
    @objc func done() {
        if let list = list {
            SectionTextField.text = "\(list[pickerView.selectedRow(inComponent: 0)].name)"}
        else {
            return
        }
        SectionTextField.endEditing(true)
    }
}

extension ViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
       
            return list?[section].tasks.count ?? 0
        
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
       // guard let count = timeCardData?.count, indexPath.row < count else { return cell }
        let cell = tableView.dequeueReusableCell(withIdentifier: "taskCell", for: indexPath)
        cell.textLabel?.text = list?[indexPath.section].tasks[indexPath.row].name
        return cell
    }
    
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        guard let list = list else {
            return "no Section"
        }
        return list[section].name
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return list?.count ?? 0
    }
}

extension ViewController: UITableViewDelegate {
    
}

extension ViewController: UIPickerViewDelegate {
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return list?.count ?? 0
    }
    
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return list?[row].name ?? ""
    }
}

extension ViewController: UIPickerViewDataSource  {
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
}


```