[SwiftUI][Realm][Xcode12] Realm を使ったアプリの開発方法(その1 : 要素作成)

     
⌛️ 3 min.
DB として CoreData が Apple から提供されていますが、Apple Platform 限定です。
CoreData 代替の有力候補である Realm を使ったアプリの作り方を説明していきます。この記事では、スキーマを定義して、その要素を表示するところまでを説明します。
Realm は、高速性やマルチプラットフォームが高く評価されている DB です。

Realm シリーズ第1回です。関係回は、以下です。

[SwiftUI][Realm][Xcode12] Realm を使ったアプリの開発方法(その2 : 要素編集)

[SwiftUI][Realm][Xcode12] Realm を使ったアプリの開発方法(その3 : UNDO/REDO 実装)

[SwiftUI][Realm][Xcode12] Realm を使ったアプリの開発方法(その4: List 表示している要素を修正する)

使用環境
  • Xcode12 beta6
  • Realm 5.3.6

プロジェクトのセットアップ

CocoaPod, Carthage を使ってセットアップすることもできますが、今回は、SPM(Swift Package Manager)を使ってセットアップしてきたいと思います。

新規プロジェクト作成

Xcode で新規プロジェクト作成 “MultiPlatform” を選択しました(今回は、実行環境の関係で iOS をターゲットとして使っていきます)。
Realm を DB レイヤーに使いますので、CoreData はチェックせずにおきます。

UnitTest はお好みで。

SPM で Realm 追加

メニューから [File]-[Swift Packages]-[Add Package Dependencies…] を選択して、SPM 追加画面を表示します。

検索フィールドに、”Realm” と入力して検索すると、候補が表示されます。

表示された候補の中から、”realm-cocoa” を探して、”Next”を押下します(複数見つかる場合は、owner が realmのものを選択してください)。

私が試した時は、最新が 5.3.6 だったので、”Version Up to Next Major 5.3.6 < 6.0.0 としました。

バージョンを指定して、”Next” を押下すると、追加するターゲットを聞いてきます。

2つのライブラリ(“Realm” と “RealmSwift”)が表示されていると思いますが、両方とも選択します。

“Finish” を押下すると設定終了です。

AppModel の設定

最近は、MVVM (Model-View-ViewModel)がよく話題になりますが、ここでは、アプリケーション用のモデルクラスを作成し、その中で Realm を扱うこととします。

DB スキーマ

TextLine という要素を定義します。TextLine は、id を持ち、テキスト情報である String を保持するものとします。スキーマとして以下のように定義しました。

また、それを束ねる要素 TextLines も定義しました。TextLines は、1要素のみを作成すると想定しているので、id を固定し、primaryKey で取得できるようにしています。

# Realm のサンプルコードを参考にしています。

Realm スキーマ


class TextLine: Object, Identifiable {
    @objc dynamic var id: String = UUID().uuidString
    @objc dynamic var text: String = ""
    
    override static func primaryKey() -> String? {
        return "id"
    }
}

class TextLines: Object {
    @objc dynamic var id: Int = 0
    let textLines = List<TextLine>()
    
    override class func primaryKey() -> String? {
        return "id"
    }
}

もちろん、Realm では、他要素との関連等も定義できますが、当面は、簡素なモデルで開発していき、徐々に拡張していこうと思います。

AppModel

単純に、DB スキーマで定義した TextLine を複数保持するものとしました。すでに保存されているようであれば、Realm から取得するようにしています。

AppModel


class AppModel: NSObject, ObservableObject {
    @Published var textLineList:List<TextLine>

    override init() {
        let realm = try! Realm()
        var textLines = realm.object(ofType: TextLines.self, forPrimaryKey: 0)
        if textLines == nil {
            textLines = try! realm.write{ realm.create(TextLines.self, value: [])}
        }
        self.textLineList = textLines!.textLines
        super.init()
    }
}

UI は、SwiftUI で構築予定なので、AppModel は、ObservableObject とし、保持している TextLine は、@Published で定義しています。

まず、ここまでのコードでコンパイルできるか確認しました。

エラーが起こるとすると原因は、SPM の設定でうまくいかなかったか、”import RealmSwift” を忘れているかだと思います。

追加コード

とりあえずのサンプルデータを作るという意味で、以下のようなコードを AppModel に追加しました。

AppModel へ追加した関数


    func addTextLine(_ text:String) {
        let newText = TextLine()
        newText.text = text
        self.addTextLine(newText)
    }
    
    func addTextLine(_ textLine: TextLine) {
        let realm = try! Realm()
        try! realm.write {
            textLineList.append(textLine)
            realm.add(textLine)
        }
    }
    public func createTestData(_ num: Int) {
        for _ in 0 .. num {
            let newText = TextLine()
            newText.text = Date().debugDescription
            self.addTextLine(newText)
        }
    }

要素追加用の関数(addTextLine) と引数で指定した数の TextLine を作成して、Realm に保存するユーティリティ関数です。




UI 作成

アプリケーションで保持するデータを AppModel として定義しました。

SwiftUI1.0 の頃は、外部で定義して、AppDelegate で渡していたと思いますが、せっかくなので、@StateObject を使って、AppModel のインスタンスのライフサイクルを管理してみたいと思います。

main

@main で定義されている App 内で、AppModel をインスタンス化して、以降の UI に渡すようにします。

コード


//
//  RealmAppApp.swift
//  Shared
//
//  Created by Tomoaki Yagishita on 2020/09/05.
//

import SwiftUI

@main
struct RealmAppApp: App {
    // 1
    @StateObject var appModel: AppModel = AppModel()
    var body: some Scene {
        WindowGroup {
            // 2
            ContentView(appModel: appModel, textLines: appModel.textLineList)
        }
    }
}

1: @StateObject を使って、インスタンス化された AppModel のライフサイクルをアプリと同じにしています。
2: 作成された AppModel の インスタンスと インスタンスプロパティ textLines を以降の UI (ContentView) に渡していきます。(EnvironmentObject を使って渡していくのもアリです)

実際に表示対象(監視対象)としたいのは、textLines です

UI

List を使って、作成された要素を表示することにします。要素のアトリビュートは、id と text しかありませんので、両方を表示するようにします。

コード


//
//  ContentView.swift
//  Shared
//
//  Created by Tomoaki Yagishita on 2020/09/05.
//

import SwiftUI

struct ContentView: View {
    var appModel: AppModel // 1
    @ObservedObject var textLines: RealmSwift.List<TextLine> //2
    var body: some View {
        VStack {
            NavigationView {
                List {
                    ForEach( textLines ) { textLine in // 3
                        Text("\(textLine.id)  \(textLine.text)") //4
                    }
                }
                .navigationBarTitle("Realm Doc") // 5
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(appModel: .init(), textLines: .init())
    }
}

1: AppModel を外部から受け取るようにしています。表示対象のリストを操作する時にこちらを経由して操作することを想定しています。
2: textLines を外部から受け取るようにしています。表示対象のリストです。
3: appModel 内部で保持されている TextLine の全てを List の対象としています。
4: TextLine で保持している id と text を表示しています。
5: ないと寂しいので(?)、Navigation のタイトルを “Realm Doc” としてみました。

ひと工夫

そのまま立ち上げても、データはありませんので、空のリストが表示されるだけです。

AppModel の初期化時に、サンプルモデルを作るようにしました。

コード


class AppModel: NSObject, ObservableObject {
    @Published var textLineList:List<TextLine>

    override init() {
        let realm = try! Realm()
        var textLines = realm.object(ofType: TextLines.self, forPrimaryKey: 0)
        if textLines == nil {
            textLines = try! realm.write{ realm.create(TextLines.self, value: [])}
        }
        self.textLineList = textLines!.textLines
        super.init()
        if textLines!.textLines.count < 4 { // 1
            self.createTestData(10)
        }
    }
<以下省略>

1: 初期化時に、要素が4つ以下であれば、追加で要素を 10 作成する。

こうしておくと、画面が 寂しく(?)ありません。

実行結果

実行すると以下のような表示がされます。

10個の要素が作成され、その要素がリスト表示されていることがわかります。

RealmDoc Ver0.1

Realm で開発するときの tips

Realm はデータを永続化するための DB ですので、一度保存したものは、永続化されています。

つまり、再起動したときに、前回のデータが再度表示されます。

もちろんこれは、正しい動作なのですが、テストの都合等でデータをクリアしたい時もあります。その方法を説明します。

データをクリアする方法は、以下の2つです。

Realm のデータをクリアする方法
  • 1: プログラム的に、要素を削除する
  • 2: 内部的に保存に使用されているファイルを削除する

1 つめの方法については、今後、紹介予定です。

今回は2つ目の方法について紹介します。

大抵、シミュレータを使ってテストしていると思いますが、シミュレータの動作しているファイルシステムは、MacOS からも見ることができます。

以下の方法で、保存場所を確認することができます。

Realm の保存場所の確認方法と削除の方法

以下の手順で保存場所を確認して、保存されたファイルを削除することができます。

Step1: アプリケーションのどこかに、ブレークポイントを設定する。今回は、AppModel の init に設定してください。
設定後、アプリケーションを起動し、ブレークポイントで止まることを確認してください。
Step2: ブレークポイントで停止後、以下のコマンドを実行する。

コード

(lldb) po Realm.Configuration.defaultConfiguration.fileURL!
file:///Users/<username>/Library/Developer/CoreSimulator/Device/<英数字列>/data/Containers/Data/Application/<英数字列>/Documents/default.realm という形でファイル位置が表示されます。
Finder で該当箇所へ移動する
オプションキーを押しながら、[移動]メニューを開くと[Library]が追加されますので、順次辿ることができます。
ファイルを削除する
default.realm ファイルだけでなく、同一フォルダにある全てのファイルを削除してください。




Realm のスキーマに変更すると、以前のデータが読み込めなくなり例外を発生することもあります。そのような時には、Migration を定義することが必要ですが、テスト目的であれば、今回説明した realm ファイルの削除で対応してしまうのも一案です。

まとめ:プロジェクトに Realm を導入して、要素を表示するまで

以下の手順で、プロジェクトに Realm を導入して、要素を作成し、表示するところまでできました。

  • SPM を使って、Realm の導入
  • Realm のモデルを定義し、データ保持と Realm への保存はアプリケーションモデルで定義
  • Realm のモデルは、通常のモデルと同様に SwiftUI で表示できる。その際、アプリケーションモデルを受け渡しする
  • tips: データの初期化は、ファイルを削除するのが簡単

おすすめの Realm 本

Web で API 等を Reference で確認することができますが、じっくりと腰を据えて学習したいときには、学習の順番が考慮されている本を使うのがおすすめです。

Realm については、本がほとんど出版されていませんが、以下の本は おすすめです。

説明は以上です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です