こんにちは。筋肉エンジニアのtakuyama29です。
今日も相変わらずベンチプレスをふんっふんっと持ち上げてから開発に取り掛かっております。
今回のこの記事では僕が先日リリースしたボディメイク専用アプリPFCボディメイクで実際に活用している検索機能について書きたいと思います。
環境
- Xcode: 8.3.3
- Swift: 3.0
- RealmSwift: 2.9.1
できたもの
実際にリリースされているアプリの一機能として活用しています。
具体的には、予めアプリ内にシードデータとしてRealmファイルをインポートしており、そこから毎回検索機能を使用する度にデータの読み込みを行ってTableViewへ出力する。と言った感じになっております。
実装までの流れ
- シードデータ作成ためにCSVデータを用意
- CSVからRealmファイルへ変換
- Xcodeプロジェクトへインポート
- Realmファイルからシードデータを読み取る
シードデータ作成ためにCSVデータを用意
まずシードデータを作成するためには、その素となるデータが必要です。
今回の実装ではシードデータとして CSV形式のデータを活用します。
CSV形式であれば作成方法は自由ですが、僕の場合は使い慣れているGooglespreadsheetを活用しました。
こんな感じ
それをこう!
これで簡単にCSVファイルができました。
なお、余談ではありますが、このシードデータの素となる全食品1612種類のPFC(たんぱく質・脂質・炭水化物)がちゃんと揃ったデータを揃えるのに丸2日間かかりました、、チョー大変だった、、
CSVからRealmファイルへ変換
これは自作で実現するのは難しいと思ったので誰か同じようなことをしてないかな?と思って探していたところ、僕がまさに欲していたライブラリがあったのでありがたく活用させていただきました。
@star__hoshi さんの Realm で CSV から初期データを作成するのealm-seed-sampleを参考にしております。
素敵な記事をありがとうございましたm(_ _)m
実際にはこのプロジェクトを書き換えてシードデータを作成したので、以下にその手順を書きます。
1. CSVファイルをインポート
まずealm-seed-sampleをcloneした後に、先ほど作成したCSVファイルをインポートします。
僕は今回 SearchExerciseList.csv
と SearchFoodList.csv
の2つをインポートしています。
2. realmファイル用のオブジェクトクラスを作成
realmファイルのシードデータを作成するには、データフォーマットを定義するObjectクラスを作成する必要があります。詳しくはRealmのドキュメントを参照ください。
これをもとに以下の様な2つのクラスを作成しました。
import RealmSwift
class SearchFoodList: Object {
dynamic var foodName = ""
dynamic var calorie = 0
dynamic var protein = 0.0
dynamic var fat = 0.0
dynamic var carbohydrate = 0.0
dynamic var objectId = ""
}
import RealmSwift
class SearchExerciseList: Object {
dynamic var exerciseName = ""
dynamic var detailInfo = ""
dynamic var mets = 0.0
dynamic var objectId = ""
}
3. CSVファイルを読み込んでrealmファイルを生成する
ここまででデータ入力用のCSVファイルと出力用のオブジェクトクラスができたので、あとはViewControllerファイル内の記述を変更しシードデータを作成します。
詳細説明は参照元の記事に書いてあるので省きます。
実際に僕のプロジェクト内でコードを書き換えたものは以下です。
import UIKit
import CSV
import RealmSwift
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let realm = try! Realm()
let fileManager = FileManager()
let seedDataRealmPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/seed.realm"
// remove realm files
if fileManager.fileExists(atPath: seedDataRealmPath) { try! fileManager.removeItem(atPath: seedDataRealmPath) }
try! realm.write { realm.deleteAll() }
// write types
let typeStream = InputStream(fileAtPath: R.file.searchFoodListCsv.path()!)!
for row in try! CSV(stream: typeStream, hasHeaderRow: true) {
print("\(row)")
let searchFoodList = SearchFoodList()
searchFoodList.foodName = row[0]
searchFoodList.calorie = Int(row[1])!
searchFoodList.protein = Double(row[2])!
searchFoodList.fat = Double(row[3])!
searchFoodList.carbohydrate = Double(row[4])!
searchFoodList.objectId = row[5]
try! realm.write {
realm.add(searchFoodList)
}
}
// write pokemon
let pokemonStream = InputStream(fileAtPath: R.file.searchExerciseListCsv.path()!)!
for row in try! CSV(stream: pokemonStream, hasHeaderRow: true) {
print("\(row)")
let searchExerciseList = SearchExerciseList()
searchExerciseList.exerciseName = row[0]
searchExerciseList.detailInfo = row[1]
searchExerciseList.mets = Double(row[2])!
searchExerciseList.objectId = row[3]
try! realm.write {
realm.add(searchExerciseList)
}
}
try! Realm().writeCopy(toFile: URL(string: seedDataRealmPath)!, encryptionKey: Data(base64Encoded: "pokemon"))
loadSeedRealm()
}
func loadSeedRealm(){
var config = Realm.Configuration()
let path = Bundle.main.path(forResource: "SeedData", ofType: "realm")
config.fileURL = URL(string:path!)
Realm.Configuration.defaultConfiguration = config
print(try! Realm().objects(SearchFoodList.self))
print(try! Realm().objects(SearchExerciseList.self))
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
※ Data(base64Encoded: "pokemon")
の pokemon
を任意の文字列に変換すると上手く動作してくれなかったので、参照のままとなっております。この事象の原因がちゃんと理解できていないのですが、それはまた後ほど。
これでrealmファイルが作成できました。
Xcodeプロジェクトへインポート
次に先の行程で作成されたrealmファイルをプロジェクト内から取り出して自分のプロジェクトへインポートします。
ですが、その際に作成したrealmファイルがどこに有るか分からず案外と苦戦しました。
これは以下の2つを参考にすることで解決できましたので、ぜひ試してみて下さい。
stack overflow Realm Browserの使い方
目的のRealmDBの中身をRealmBrowserで一瞬で開く方法
これで見つけ出したファイルをプロジェクトへインポートしてシードデータの準備は完了です。
Realmファイルからシードデータを読み取る
最後にプロジェクトへインポートしたシードデータを読み取ってアプリ内で使用できる状態にするための実装を行います。
このシードデータはアプリ内で使用(realmファイルへアクセス)する度にConfigurationを指定して読み込みをしなければなりません。
realmファイルへアクセスする際のConfiguration指定
そもそもrealmファイルのパス指定やファイルに対するアクセス権などを設定するにはRealm(configuration: config)
メソッドかもしくは、Realm.Configuration.defaultConfiguration = config
メソッドを使用する必要があります。
ここで注意が必要なのは、シードデータは基本的に書き込み不可なので、シードファイルへのアクセス時にreadOnly
と設定する必要がありますが、その際に上述のRealm.Configuration.defaultConfiguration
にreadOnly
をセットしてしまうと、Realmファイルを使用する際にデフォルト設定が書き換わってしまい、他の箇所でRealmファイルへアクセスしようとした際にも読み取り専用となってしまいます。
詳細はRealmの設定を変更するを参考にしていただければと思いますが、基本的にこのシードデータに対する設定変更はRealm(configuration: config)
で行うようにします。
具体的には、シードデータへアクセスする際の設定は以下のようになります。
// シードデータのConfiguration指定
let config = Realm.Configuration(fileURL: Bundle.main.url(forResource: "seed", withExtension: "realm"),readOnly: true)
// Configurationを適用
let realm = try! Realm(configuration: config)
なお、この設定はシードデータへアクセスする度に処理してあげないと、デフォルトの設定が自動的に適用される様になってしまい、シードデータが読み込まれなくなってしまいます。
realmファイルをインスタンス化
次にシードファイルから読み取ったデータをインスタンス化するためのObjectクラスを作成します。
先にCSVからシードデータを作成した際と同様に、以下の様にクラスを定義します。
import RealmSwift
// 食品検索リストクラス
class SearchFoodList: Object {
dynamic var foodName = ""
dynamic var calorie = 0
dynamic var protein = 0.0
dynamic var fat = 0.0
dynamic var carbohydrate = 0.0
dynamic var objectId = ""
}
import RealmSwift
// 運動検索リストクラス
class SearchExerciseList: Object {
dynamic var exerciseName = ""
dynamic var detailInfo = ""
dynamic var mets = 0.0
dynamic var objectId = ""
}
あとはこのクラスをインスタンス化してデータ利用すればOKです。
また、必要に応じてフィルターを掛けることで、任意のデータのみを検索して取得できます。
フィルターについてはクエリを参考にして下さい。
import RealmSwift
class AnyViewController: UIViewController {
...
func anyFunc() {
// シードデータのConfiguration指定
let config = Realm.Configuration(fileURL: Bundle.main.url(forResource: "seed", withExtension: "realm"),readOnly: true)
// Configurationを適用
let realm = try! Realm(configuration: config)
// SearchFoodListをインスタンス化
let results = realm.objects(SearchFoodList.self)
// SearchExerciseListクラスのexerciseNameの中で"word"を含むObjectを検索
let results = realm.objects(SearchExerciseList.self).filter("exerciseName LIKE %@", "*\(word)*")
}
...
}
もしこの手順で上手くいかないパターンなどがあった場合はお手数ですがご報告いただければと思います。説明の中に抜け漏れがあれば説明いたします。
また、内容について誤っているものがあればご指摘いただければと思います。