iOSアプリのマスタデータ管理にRealmを採用したので紹介します!
この記事は「ニフティグループ Advent Calendar 2023」 23日目の記事です。
こんにちは!ニフティライフスタイルでモバイルアプリの開発をしているsaikeiです。
今回は、iOSアプリのマスタデータをRealmで管理する方法についてご紹介します!
目次
はじめに
iOSアプリでも、駅や市区町村情報のような「マスタデータ」を扱う機会は珍しくないと思います。
また、その手法も用途や要件により様々ありますが、今回は弊社の一部アプリでも採用している「Realmを用いたマスタデータの取り扱い」についてご紹介します!
マスタデータをRealmで管理することにした背景
前提として、今回のアプリでのマスタ管理は下記要件となっていました。
- マスタデータにリアルタイム性は求められない
- 定期的にリモートから取得するなどの処理は不要
- アプリのアップデート時に必要に応じて更新されれば良い
- アプリの根幹となる機能のためマスタデータが正しく取得できない場合は、アプリが起動出来ないようにしたい
そのため、要件を満たす方法を検討し「リアルタイムで更新されるものではない」という点や、「初回起動やアップデート時に待機時間を極力発生させない」という観点からRealmを採用することになりました。
手順
1. アプリにRealm Swiftを導入し、Entityを定義する
公式サイトを参考にアプリに「Realm Swift」を追加し、テーブル定義(Entity)を実装します。
今回は以下の「都道府県Entity」を例に進めていきます。
// 都道府県Entity
final class PrefectureEntity: Object {
@objc dynamic var code = ""
@objc dynamic var name = ""
convenience init(
code: String,
name: String
) {
self.init()
self.code = code
self.name = name
}
}
2. Realmファイルを生成する
Entityを定義した後にアプリをビルドすることでRealmファイルを生成することができます。
ユーザーデータ(検索条件や各種履歴・設定値など)の上書きを防ぐためにデータベースを分ける必要があるため、Realmの初期化時に任意のファイル名を指定します。
var config = Realm.Configuration.defaultConfiguration
let masterRealmURL = config.fileURL!.deletingLastPathComponent().appendingPathComponent("master.realm")
config = Realm.Configuration(
fileURL: masterRealmURL,
schemaVersion: config.schemaVersion,
migrationBlock: config.migrationBlock
)
config.objectTypes = [
PrefectureEntity.self // マスタで使用したいEntityを宣言
]
// swiftlint:disable:next force_try
let realm = try! Realm(configuration: config)
3. Realmファイルを取り出す
「テーブル定義が実装されている空のRealmファイル」が必要になるため、シミュレーターでビルドした上で「2のコード」の masterRealmURL
を出力します。
該当のパスをFinderで開き master.realm
を一度任意の場所にコピーします。
4. マスタデータを入れたRealmファイルを作る
公式で提供されている「Realm Studio」というツールを使うことで、CSV形式のデータをRealmファイルに流し込むことができます。
今回の「都道府県マスタ」では以下のようなCSVファイルを流していきます。
code,name
01,北海道
02,青森県
…
46,鹿児島県
47,沖縄県
5. アプリにバンドルする
作成したRealmファイルをアプリにバンドルする必要がありますが、直接「Realmから参照できる場所」に置くことが出来ないため、一旦プロジェクト内に配置した上で「Realmから参照できる場所」にコピーしていきます。
以下はアップデート時にマスタのコピーが失敗した時のために、一時ファイルを経由してコピーするコード例です。
// バンドルしている master.realm のパスを取得
guard let bundleMasterRealmPath = Bundle.module.url(forResource: "master", withExtension: "realm") else {
fatalError("bundleMasterRealmPathが取得できませんでした")
}
// アプリで使用する master.realm のパスを取得
guard let masterRealmPath = Realm.Configuration.defaultConfiguration.fileURL?
.deletingLastPathComponent().appendingPathComponent("master.realm") else {
fatalError("masterRealmPathが取得できませんでした")
}
let tmpFilePath = masterRealmPath.deletingLastPathComponent().appendingPathComponent("tmp-master.realm")
let backupFilePath = masterRealmPath.deletingLastPathComponent().appendingPathComponent("backup-master.realm")
// master.realmが存在しない時はmaster.realmをコピーする
guard fileManager.fileExists(atPath: masterRealmPath.path) else {
do {
try copyBundleMasterRealm(bundleMasterRealmPath, to: masterRealmPath)
} catch {
fatalError("master.realmのコピーに失敗しました")
}
}
// master.realmが既に存在する時は置換する
do {
// バンドルmaster.realm を tmp-master.realm としてコピー(同名ファイルが存在する場合は削除)
try copyBundleMasterRealm(bundleMasterRealmPath, to: tmpFilePath)
// 実master.realm を backup-master.realm にリネーム(同名ファイルが存在する場合は削除)
try renameBundleMasterRealm(masterRealmPath, to: backupFilePath)
// tmp-master.realm を master.realm にリネーム(同名ファイルが存在する場合は削除)
try renameBundleMasterRealm(tmpFilePath, to: masterRealmPath)
// バックアップファイルを削除
try fileManager.removeItem(at: backupFilePath)
// END - 処理を終了
} catch {
// ここでエラー処理
}
※ 上記コード例では fatalError
としていますが、必要に応じて適切なエラー処理を行ってください。
6. マスタのアップデート対応
マスタ情報にアップデートがある場合は、アプリ内のマスタを更新する必要が出てきます。
単純に毎起動ごとに「手順5」のコピーを行なうのも手段の一つではありますが頻繁に更新されるマスタではないため、弊社では「マスタが更新された時のみ更新(マスタの置き換え)」を行うように工夫しています。
具体的には以下のようにバージョンを管理しておりアプリのアップデート時に2つのバージョンを比較し、マスタのアップデートを実施しています。
info.plist
: バンドルしているマスタのバージョン- マスタを更新した際にインクリメントする
UserDefaults
: アプリ内で使用しているマスタのバージョン- 「手順5」が成功した時点のマスタバージョンを保存する
おわりに
今回はRealmを用いたマスタデータの管理についてご紹介しましたが、冒頭でも記載の通りマスタデータの扱いは様々な手法があり、企業やサービスごとに最適な方法を採用していると思います。
Realmを採用しているパターンをとっても、リモートから取得して都度インサートしていたり、CSVファイルをバンドルして起動時にインサートするといったパターンなど複数のケースがあると思います。
現時点では今回の方法が良いと思っていますが、今後新たに出てくる要件や、Swift周りの新しい技術、サードパーティライブラリなどで環境は変わっていくと思うのでその時々に応じたベストな方法を引き続き模索してプロダクト開発をしていきたいと思います。
この記事を見ていただいた方の参考になれば幸いです。
最後までお読みいただきありがとうございました!
掲載内容は、記事執筆時点の情報をもとにしています。