[swift3]RealmSwiftを使ってシードデータを作成し、アプリ内検索機能を実装する

こんにちは。筋肉エンジニアのtakuyama29です。
今日も相変わらずベンチプレスをふんっふんっと持ち上げてから開発に取り掛かっております。

今回のこの記事では僕が先日リリースしたボディメイク専用アプリ[**PFCボディメイク**](https://itunes.apple.com/jp/app/pfc%E3%83%9C%E3%83%87%E3%82%A3%E3%83%A1%E3%82%A4%E3%82%AF/id1274048262?mt=8)で実際に活用している検索機能について書きたいと思います。

# 環境
- Xcode: 8.3.3
- Swift: 3.0
- RealmSwift: 2.9.1

# できたもの
実際にリリースされているアプリの一機能として活用しています。

![searchTableView.gif](https://qiita-image-store.s3.amazonaws.com/0/143619/c66584a5-d159-15fe-7ffe-f848b2645aff.gif)


具体的には、予めアプリ内にシードデータとしてRealmファイルをインポートしており、そこから毎回検索機能を使用する度にデータの読み込みを行ってTableViewへ出力する。と言った感じになっております。

# 実装までの流れ
1. シードデータ作成ためにCSVデータを用意
2. CSVからRealmファイルへ変換
3. Xcodeプロジェクトへインポート
4. Realmファイルからシードデータを読み取る

# シードデータ作成ためにCSVデータを用意
まずシードデータを作成するためには、その素となるデータが必要です。
今回の実装ではシードデータとして　CSV形式のデータを活用します。
CSV形式であれば作成方法は自由ですが、僕の場合は使い慣れているGooglespreadsheetを活用しました。

こんな感じ

<img width="1045" alt="Screen Shot 2017-09-03 at 20.38.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/143619/0cd27095-87a6-e5a7-4e1c-d102603194b5.png">

それをこう！

<img width="1053" alt="Screen_Shot_2017-09-03_at_20_39_53.png" src="https://qiita-image-store.s3.amazonaws.com/0/143619/f4898d8e-33d5-245f-b012-36338a508be6.png">

これで簡単にCSVファイルができました。

なお、余談ではありますが、このシードデータの素となる全食品1612種類のPFC(たんぱく質・脂質・炭水化物)がちゃんと揃ったデータを揃えるのに丸2日間かかりました、、チョー大変だった、、

# CSVからRealmファイルへ変換
これは自作で実現するのは難しいと思ったので誰か同じようなことをしてないかな？と思って探していたところ、僕がまさに欲していたライブラリがあったのでありがたく活用させていただきました。

@star__hoshi さんの [Realm で CSV から初期データを作成する](http://qiita.com/star__hoshi/items/90c9c54301027b01ffa2)の[ealm-seed-sample](https://github.com/starhoshi/realm-seed-sample/tree/master)を参考にしております。

素敵な記事をありがとうございましたm(_ _)m

実際にはこのプロジェクトを書き換えてシードデータを作成したので、以下にその手順を書きます。

### 1. CSVファイルをインポート
まず[ealm-seed-sample](https://github.com/starhoshi/realm-seed-sample/tree/master)をcloneした後に、先ほど作成したCSVファイルをインポートします。
僕は今回 `SearchExerciseList.csv` と `SearchFoodList.csv` の2つをインポートしています。

<img width="782" alt="Screen_Shot_2017-09-03_at_21_01_08.png" src="https://qiita-image-store.s3.amazonaws.com/0/143619/e8a5ab18-586d-1df6-1422-f42a59faef33.png">


### 2. realmファイル用のオブジェクトクラスを作成
realmファイルのシードデータを作成するには、データフォーマットを定義するObjectクラスを作成する必要があります。詳しくは[Realmのドキュメント](https://realm.io/jp/docs/swift/latest/#section-23)を参照ください。

これをもとに以下の様な2つのクラスを作成しました。

```SearchFoodList.swift
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 = ""
}
```

```SearchExerciseList.swift
import RealmSwift

class SearchExerciseList: Object {
    dynamic var exerciseName = ""
    dynamic var detailInfo = ""
    dynamic var mets = 0.0
    dynamic var objectId = ""
}
```

### 3. CSVファイルを読み込んでrealmファイルを生成する
ここまででデータ入力用のCSVファイルと出力用のオブジェクトクラスができたので、あとはViewControllerファイル内の記述を変更しシードデータを作成します。

詳細説明は参照元の記事に書いてあるので省きます。

実際に僕のプロジェクト内でコードを書き換えたものは以下です。

```ViewController.swift
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の使い方](https://ja.stackoverflow.com/questions/8592/realm-browser%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9)
[目的のRealmDBの中身をRealmBrowserで一瞬で開く方法](http://qiita.com/gologo13/items/5cb94ffd67e3b09f7678)

これで見つけ出したファイルをプロジェクトへインポートしてシードデータの準備は完了です。

# Realmファイルからシードデータを読み取る
最後にプロジェクトへインポートしたシードデータを読み取ってアプリ内で使用できる状態にするための実装を行います。

このシードデータはアプリ内で使用(realmファイルへアクセス)する度にConfigurationを指定して読み込みをしなければなりません。

### realmファイルへアクセスする際のConfiguration指定

そもそもrealmファイルのパス指定やファイルに対するアクセス権などを設定するには`Realm(configuration: config)`メソッドかもしくは、`Realm.Configuration.defaultConfiguration = config`メソッドを使用する必要があります。

ここで注意が必要なのは、シードデータは基本的に**書き込み不可**なので、シードファイルへのアクセス時に`readOnly`と設定する必要がありますが、その際に上述の`Realm.Configuration.defaultConfiguration`に`readOnly`をセットしてしまうと、Realmファイルを使用する際にデフォルト設定が書き換わってしまい、他の箇所でRealmファイルへアクセスしようとした際にも読み取り専用となってしまいます。

詳細は[Realmの設定を変更する](https://realm.io/jp/docs/swift/latest/#realm-3)を参考にしていただければと思いますが、基本的にこのシードデータに対する設定変更は`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からシードデータを作成した際と同様に、以下の様にクラスを定義します。

```SearchFoodList.swift
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 = ""
}
```
```SearchExerciseList.swift
import RealmSwift

// 運動検索リストクラス
class SearchExerciseList: Object {
    dynamic var exerciseName = ""
    dynamic var detailInfo = ""
    dynamic var mets = 0.0
    dynamic var objectId = ""
}
```

あとはこのクラスをインスタンス化してデータ利用すればOKです。
また、必要に応じてフィルターを掛けることで、任意のデータのみを検索して取得できます。

フィルターについては[クエリ](https://realm.io/jp/docs/swift/latest/#section-28)を参考にして下さい。

```
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)*")

    }

    ...

}

```

もしこの手順で上手くいかないパターンなどがあった場合はお手数ですがご報告いただければと思います。説明の中に抜け漏れがあれば説明いたします。

また、内容について誤っているものがあればご指摘いただければと思います。