やってみたらなんとかなる

プログラミングをする上で調べたこととかやったこととか

[Swift]RealmSwiftでデータベースを使いたい<実践編>

概要

Swiftでデータベースを使いたい。<準備編>, <基本編>の続き。

環境

  • Xcode: 12.1
  • Swift: 5.3
  • Realm: 10.1.1
  • RealmSwift: 10.1.1

今回(と前回)で作るアプリ

入力した文字列をリストに追加していくアプリを作ってみる

f:id:yuyuyu_331:20201105131223p:plain
RealmSwiftTestApp

実際に組み込んでみる

前回の<基本編>で書いたRealmSwiftのコードを実際にアプリケーションの中に組み込んでみます。

SwiftUIを使って組み込みます。

SwiftUIの説明はがっつり省きます。後日まとめると思います多分。

前提となるレコードの型や関数

前回の説明に出てきた方々です。

class ListComponent: Object {
    @objc dynamic var word = ""
}

func addRecord (component: String) { let realm = try! Realm() let record = ListComponent() record.word = component try? realm.write { realm.add(record) } }

func delRecord (record: ListComponent) { let realm = try! Realm() try? realm.write { realm.delete(record) } }

レコードのリスト表示

レコードを全件取得してそれをリスト表示します。

基本的にはレコードを取得した後、それをForEachで回すイメージです。

List {
    ForEach(realm.objects(ListComponent.self), id: \.self) { record in
        Text(record.word)
    }
}

これでOKです。

データベースの更新をリストに反映

データベースの監視

今回のアプリでは、入力された文字列をリストに追加していくため、データベースにレコードが追加された時にまたレコードを取得し、リストを更新する必要があります

そのためにレコードの変化を監視するobserveメソッドを使います。

データベースのレコードを取得するモデルを作成し、その中で監視するといった方法をとります。

class ListComponentViewModel: ObservableObject {
    private var token: NotificationToken?
    private var componentResults = try? Realm().objects(ListComponent.self)
    @Published var components: [ListComponent] = []
    init() {
        token = componentResults?.observe { [weak self] (changes: RealmCollectionChange) in
            switch changes {
            case .initial:
                self?.components = self?.componentResults?.map { $0 } ?? []
            case .update(let record, deletions: let deletion, insertions: let insertion, modifications: _):
                if deletion != [] {
                    for index in deletion {
                        self?.components.remove(at: index)
                    }
                }
                if insertion != [] {
                    for index in insertion {
                        self?.components.append(record[index])
                    }
                }
            case .error(_):
                print("error")
            }
        }
    }
    deinit {
        token?.invalidate()
    }
}

うわぁ。。。なんだこのコード読む気すらおきない。。。

と思ったそこのあなた。間違い無いです。ごめんなさい

一個ずつ説明していきます。

NotificationTokenというクラスで、コレクション(レコードの集まり)を監視するようです。NotificationTokenはコレクションのobserveメソッドで得ることができます。

observeメソッド中でコレクションの変化毎に行うことを書きます。ちなみに、observeメソッドは、オブジェクト(レコード)、コレクション、Realm全体それぞれにあるらしく(ここに書いてあります)、コレクションだと、「コレクションへの挿入(insertion)」「コレクションからの削除(deletion)」「コレクションないの変更(modification)」の三種類を監視しているみたいです。

その三種類の内どれかが行われた時にobserveメソッドが呼び出されるようになってるみたいです。呼び出された種類によって.update内の引数に指定された変数の中に変更があった部分のインデックスがArrayで格納されます。

8番目に挿入されたならinsertionsに指定された変数(今回はinsertion)に[8]が格納されます。

6番目のレコードが削除されたならdeletionsに指定された変数(今回はdeletion)に[6]が格納されます。

また.updateの最初の引数には変更後のコレクションそのものが格納されます。

.initialは最初に取得した時に実行されるみたいです。(正直ここはよくわかってません)今回では、コレクションを配列として扱いたいのでコレクションを取得して配列に変換してます。

.errorは多分エラーが出た時に実行されます。(ここはほんとにわかんないです。勘)

リスト表示

レコードの受け取り方を変えたのでリスト表示の方も少しだけ変えます。

@ObservedObject var model = ListComponentViewModel()
List {
    ForEach(model.components, id: \.self) { record in
        Text(record.word)
    }
}

ついでにリストからの削除も実装しておきます。

List {
    ForEach(model.components, id: \.self) { record in
        Text(record.word)
    }
    .onDelete{ indexSet in
        if let index = indexSet.first {
            delRecord(record: self.model.components[index])
        }
    }
}

入力部分

ここはもうほとんどSwiftUIの基礎そのままみたいな感じですかね。

TextFieldで文字入力を受け付けて、Buttonでそれをデータベースに追加するといった感じです。

@State private var word = ""

HStack { TextField("", text: $word) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding(10) Button(action: { if word != "" { addRecord(component: word) word = "" } }, label: { Text("ADD") }) .padding(10) }

ここまでをまとめる

ここまで出てきたものを全部まとめて形を整えるとこんな感じになります。

import SwiftUI
import RealmSwift

class ListComponent: Object { @objc dynamic var word = "" }

class ListComponentViewModel: ObservableObject { private var token: NotificationToken? private var componentResults = try? Realm().objects(ListComponent.self) @Published var components: [ListComponent] = [] init() { token = componentResults?.observe { [weak self] (changes: RealmCollectionChange) in switch changes { case .initial: self?.components = self?.componentResults?.map { $0 } ?? [] case .update(let record, deletions: let deletion, insertions: let insertion, modifications: _): if deletion != [] { for index in deletion { self?.components.remove(at: index) } } if insertion != [] { for index in insertion { self?.components.append(record[index]) } } case .error(_): print("error") } } } deinit { token?.invalidate() } }

func addRecord (component: String) { let realm = try! Realm() let record = ListComponent() record.word = component try? realm.write { realm.add(record) } print("add") }

func delRecord (record: ListComponent) { let realm = try! Realm() try? realm.write { realm.delete(record) } }

func changeRecord (record: ListComponent, component: String) { let realm = try! Realm() try? realm.write { record.word = component } }

struct ContentView: View { @State private var word = "" @ObservedObject var model = ListComponentViewModel() let realm = try! Realm() var body: some View { VStack { HStack { TextField("", text: $word) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding(10) Button(action: { if word != "" { addRecord(component: word) word = "" } }, label: { Text("ADD") }) .padding(10) } List { ForEach(model.components, id: \.self) { record in Text(record.word) } .onDelete{ indexSet in if let index = indexSet.first { delRecord(record: self.model.components[index]) } } } } } }

struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }

やったことだけじゃなくてテンプレートとかその他もろもろがくっついてきてるんですけどね。

これで目的のアプリケーションの関西です。お疲れ様でした。

まとめ

RealmSwiftについての文書が少なすぎる。全然わからんかった。よく頑張った私。

参考にさせていただいた記事