RealmSwift入門 -Realm Platform 管理画面からアプリのデータを変更するまで-

こんにちは。モバイルアプリサービス部に所属する田辺です。前回の Realm Platform を扱った記事で紹介したコードだと開発用途では問題がないものの本番環境では問題のある実装や設定がいくつかあります。認証や権限周りがそれに当たります。

今回は実際にサーバー側からファイルを変更してアプリ側の表示も変更されるまでを、認証や権限のセキュリティを最低限考慮しつつ実装していきたいと思います。

過去のRealmSwiftに関する記事

どんなアプリか

クラウド上にあるデータベースから取得したデータを TableView に表示するだけのアプリです。サーバー側からファイルを変更したらその変更を検知してアプリ側の表示も更新されるように実装します。

Realm の同期プロセスはFull Synchronizationを選択します。サーバー側からのデータベースの中身を更新するのが目的です。クライアント側からサーバーにたいして新規にデータを作成・及び既存のデータの削除は行いません。

実装

認証

前回の記事で紹介したnickname認証はパスワードを使用しない開発用の認証方法です。今回はパスワードも入力させるようにします。

let creds = SyncCredentials.usernamePassword(username: nameTextField.text!, password: passTextField.text!, register: self.isNewUser)

SyncUser.logIn(with: creds, server: Constants.AUTH_URL, onCompletion: { [weak self](user, err) in
    if let _ = user {
        self?.navigationController?.pushViewController(ItemsViewController(), animated: true)
    } else if let error = err {
        let alert = UIAlertController(title: "\(self!.isNewUser ? "登録" : "ログイン")失敗", message: error.localizedDescription, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        self?.present(alert, animated: true, completion: nil)
    }
})

他にも JWT 認証やサードパーティの認証の仕組みを利用する事ができます。認証に関するドキュメントは以下になります。

Full-Sync permissions

Full Synchronizationによってデータ同期を行う場合、データアクセスは Realm に割り当てられた特定のパスに基づいて個々の Realm で制御できます。 例えば、グローバルな読み取り専用の Realm と各ユーザーが管理するユーザー固有の Realm があるという状態で管理するといった具合です。

global な readonly Realm の作成の前に

You do not need to create the default synced Realm, it is automatically created for you.

Setting Up Your Realms - Realm Sync Docs

Realm Sync Docs に記述されている通り自動で作成されるため default sunced Realm をこちら側で作る必要はありません。

しかし global な readonly Realm はこちらで予め用意しているものなので、それとクライアント側を同期するにはいくつか設定が必要です。

設定の前に事前知識として Raelm Platform ではどのようにユーザーごとの Permission を管理しているのか理解しているとこの後の設定がスムーズに頭に入ると思うので説明させてください。

Realm Platform ではアプリケーションのデータに加えて管理用の内部クラスとそれに所属するデータを持っています。

ひと目で分かるように Realm Studio に内部クラスを表示するように設定してみました。画像が以下になります。

root path 以下に__を prefix にしている path がいくつもあるのが確認出来るかと思います。この path で Realm Platform は内部クラスやそれに所属するデータ(リスト)を管理しています。そして下の方には/以下にランダムな文字列のパスがありその下にも内部クラスを管理している__を prefix にしたパスが存在しています。このパスはこちら側で作ったものではなくユーザー作成時に自動的に作られるものです。このことからディレクトリごとに権限管理をしているのがわかります。

__adminという path を見てみます。

Realm Studio では左側のペインにクラス一覧が表示され、選択すると右側のペインにフィールドと登録されているデータが一覧で表示されます。

このページではアカウントの管理をしていててユーザーごとの認証方法を管理しているみたいです。先程実装したパスワード認証を使用して3つのアカウントが登録されているのが確認できます。

Permission クラスのフィールドがこんな感じです。データはモザイクで隠しています。

このクラスで root path のディレクトリの権限管理を行っています。ここまでを踏まえて default で readonly Realm を作成してみます。

global な readonly Realm の作成

最初は global な Realm を作成してその後ユーザーの作成ごとに読み取り権限を与えるのかと思っていました。 特定の path の何らかの権限を user に付与するには以下のような実装を行います。

let permission = SyncPermission(realmPath: realmPath,  // The remote Realm path on which to apply the changes
                                identity: anotherUserID, // The user ID for which these permission changes should be applied
                                accessLevel: .write)   // The access level to be granted
user.apply(permission) { error in
    if let error = error {
        // handle error
        return
    }
    // permission was successfully applied
}

しかし権限を渡す user が root path にある global Realm の権限を持っているか Administrator でないと新たに作成した user(Administrator に対して Regular User と呼びます)に global Realm への読み取り権限を与えることができません。

RealmSwift ではSyncUser.currentでログイン中のユーザーを取り出せます。またSyncUser.allというプロパティがあり、ユーザーの切替が可能です。 SyncUser.all の中に Admiministrator の権限を持つ user を含めて一時的に切り替えて作成されたアカウントに global Realm への権限を付与することはできました。 しかし、Administrator の権限を持つ user をアプリをインストールしたユーザーが持つのは適切ではありません。

少し調べてみると フォーラムや realm-object-server の Github リポジトリでのやり取りから、現在 global Realm の読み取り権限付与は default では有効になっていないことがわかりました。Realm Platform の root path の permission 一覧にも確かにそのような権限は存在しません。

※参考

そこで今回は管理画面から global path にある特定の Realm の default の読み取り権限を有効にします。

Realm Studio から先程の__adminを開き Permission クラスを選択します。その後右上のCreate Permissionと記載されているボタンをクリックします。

読み取り権限のみを与えたい global Realm を realmFile というフィールドで選択します。

その後 mayRead というフィールドを true にして作成します。これで realmFile で選択した global Realm が読み取りのみ public に許可されました。

最初の同期後接続が切断される問題への対応

Readonly の global Realm との Sync が最初行われたあとすぐに接続が切断されてしまう現象に一度なってしまいました。

Readonly Synced Realm Opens, But Closes Immediately · Issue #4485 · realm/realm-cocoa を参考に、 __adminの Permission に残っていたユーザーごとの読み取り権限を全て消すと接続が切断されることはありませんでした。この権限はコードで global Realm の権限付与を行った時に消し忘れていたものでした。

この時内部で何が起きていているのか想像は出来たのですが仕様として記述されている箇所がドキュメントで見つからなかったので、曖昧な情報を記載するのは控えたいと思います。見つかり次第記事を更新いたします。

表示

権限周りの設定が済んだのでコードに戻ります。

基本的にはサンプルプロジェクトと同じ tableView にデータを表示するだけです。前回の記事で説明したFull Synchronizationで必要な実装を忘れず行います。

override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
    let config = SyncUser.current?.configuration(realmURL: Constants.REALM_URL, fullSynchronization: true)
    self.realm = try! Realm(configuration: config!)
    self.items = realm.objects(Item.self).sorted(byKeyPath: "timestamp", ascending: false)
    super.init(nibName: nil, bundle: nil)
}

viewDidload()ではデータが更新された時の処理を書きます。

override func viewDidLoad() {
    super.viewDidLoad()
    title = "Things ToDo!"
    view.addSubview(tableView)
    tableView.frame = self.view.frame
    self.tableView.dataSource = self
    navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Logout", style: .plain, target: self, action: #selector(rightBarButtonDidClick))

    notificationToken = items.observe { [weak self] (changes) in
        guard let tableView = self?.tableView else { return }
        switch changes {
        case .initial:
            tableView.reloadData()
        case .update(_, let deletions, let insertions, let modifications):
            tableView.beginUpdates()
            tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
                                 with: .automatic)
            tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
                                 with: .automatic)
            tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
                                 with: .automatic)
            tableView.endUpdates()
        case .error(let error):
            fatalError("\(error)")
        }
    }
}

deinit()でNotificationTokenを破棄します。

deinit {
    notificationToken?.invalidate()
}

UITableViewDataSource protocol に準拠するためtableView(_:cellForRowAt:)メソッドとtableView(_:numberOfRowsInSection:)の実装をしていますが、ここは Realm Platform 特有の実装はないので割愛します。サンプルコードは最後にリポジトリへのリンクを貼るので参照したい方はそちらからお願いします。

実際に作成したり削除したりしてみる

うまくいってますね。

まとめと課題

Realm Platform のドキュメントはまだまだあるので今後もっと詳しい仕様を把握して適切な設定と実装ができるようになりたいです。

また Realm Cloud の一番安い料金プランだと同時接続 10000 が限度になっているので、更新の極めて少ない Realm ファイルなら段階的な同期とかが出来ないか試してみたいと思います。

関連リンク