TableViewの勉強を進めていると、Sectionが複数になった途端コードが散らかってしまいませんか?
2次元配列にしたり、Sectionクラスを作ってプロパティとしてrow要素を組み込んだり方法は色々ありますが、Realm の逆方向の関連を使うと比較的綺麗にまとまったので紹介します。
Sectionとrowだけでなく、一対多のネスト構造のもの全般に活躍できそうです。例えば、メインオブジェクトはゲームソフトだけど、新規作成はゲームの記録をメインとしたUIを想定している場合でも、Realmの逆方向の関連を活用すればUIに沿った形でコードが作成できるかと思います。
今回は、Realmの逆方向の関連を用いて、簡単なTodoアプリの新規作成機能を作りたいと思います。
環境
Swift 5.2
Xcode 11.4
Realm 10.4.0
Realmのインストールが済んだ状態から進めます。
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は例えばこのような感じになります。
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
}
}