[iOS] Realm ハンズオン #1 の復習(導入，保存まで)


## はじめに
今年の 3 月中旬にあった iOS の Realm のハンズオンの復習をボチボチやらないとーということで書いてみました。まだ理解が乏しいところもあるので曖昧なところもあると思いますが，ご指摘いただければと思います。ごちゃごちゃ書いてみにくい記事になりました。申し訳ありません。行きたかったけど行けなかった方，少しやってみようと思っている方の役に立てばいいなー。

あの日は結構準備段階で時間を使って，Realm を触る時間が少なくなっていたのである程度作ったやつがあったらよかったのかなーと思いました。

勉強会レポ(概要)書いてます。よろしければどうぞ。
[【勉強会レポ160312008】Realm ハンズオン(iOS) #1](http://milanista224.blogspot.jp/2016/03/repo-160312008.html)


## Realm とは
Qiita にも公式サイトにも有益な情報がたくさんです。
詳しくは省きますが，こんな感じでしょうか。

>Realm is a replacement for SQLite & Core Data.
It can save you thousands of lines of code & weeks of work,
and lets you craft amazing new user experiences.

* SQLite や Core Data に変わる次世代データベース
* 無料で使える
* 最初の学習コストが少ないので導入しやすい
* 岸川さん曰くまずは検討してみてくれとのこと

**Realm に SQLite のデータを移行できるか気になる。**

## 用意するもの
* Mac (El Capitan)
* Xcode 7.3 系(書いたとき Xcode 7.3.1，Xcode 7.3.2 でも一応)
* CocoaPods 0.39.0 以降
* git の知識(少し)
* 今回は実機じゃなくてもできます

## Twitter Realm ハンズオン

あの日同様，手を動かしながらやっていきます。
Twitter，Realm に関連しない部分までを GitHub に用意しました。
master ブランチをチェックアウトして動作確認してみてください。

[GitHub リンク](https://github.com/MilanistaDev/RealmHandsOnSample.git)

ここまでの実装で気になる方は master ブランチのコミット履歴をごらんください。
TimeLine と Favorites Tweet が 2 つのタブに適用されているはずです。

```bash:ターミナル
$ cd ~/Downloads(など適切な場所)
$ git clone https://github.com/MilanistaDev/RealmHandsOnSample.git
$ cd RealmHandsOnSample/
$ git checkout master
```

git と cocoapods は使えるようにしておいてください。
変数宣言時に多くが型まで指定していますが，わかりやすくするためです。明らかに明確である場合省いても構いません。

## CocoaPods から Realm 導入
今回は，Swift 2.2 でやります。Podfile は用意しておきましたので，master ブランチにチェックアウトして，pod install してください。
対象コミットは Github の **654aa71ef7c2fd4d3d2f0dd410423744c8bba124**

CocoaPods は 0.39.0 以降対応のようです。
この前 v1.0.0 が出たのでそれでいいです。

```bash:ターミナル
$ cd RealmHandsOnSample/
$ git checkout master
$ pod install
```
他にも直接ドラッグアンドドロップで導入する方法と，Carthage(カルタゴ)で導入する方法があるようです。

公式サイト参照してください。

[https://realm.io/docs/swift/latest/](https://realm.io/docs/swift/latest/)

この後の実装はブランチを分けています。 **feature/twitter-realm** で実装していますので参考にしてください。Github で diff(差分) を見るのがいいと思います。

## Realm が使えるか確かめてみる
**コミット：e4887d415febe121cca1cd38bd9bcc31d49635a0**

AppDelegate.swift に RealmSwift を import して，一番上のメソッド内に下記のように書いてみる。

```Swift:AppDelegate.swift
import RealmSwift
```

```Swift:AppDelegate.swift
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    UINavigationBar.appearance().titleTextAttributes = [NSForegroundColorAttributeName:UIColor.whiteColor()]

    // インスタンスの生成
    let realm = try! Realm()
    print(realm)

    return true
}
```

実行してみます。
下記がコンソール部分に出力されると思います。

> RealmSwift.Realm

これが表示されたらうまく導入できたということになります。

## ATS 対策
**コミット：3fef2110fde74bb44b031057dd680d2b474125b0**

Twitter の API 叩くのに HTTP になるので ATS 対策しておく。
ATS を無効にしておきます。わからない方は下記 URL 参照してください。
[ATS対策(Xcode7, iOS 9)](http://qiita.com/MilanistaDev/private/71e69e51f45519f52c3f)


## Twitter 部分の実装
**コミット：3fef2110fde74bb44b031057dd680d2b474125b0**

ここは Realm とは直接関係ないのですが，認証系とツイートを取得するとこやります。

TimeLineViewController.swift に下記の 2 つを import します。

```Swift:TimeLineViewController.swift
import Accounts
import Social
```

下記プロパティを追加。timeLine に twitter 情報が格納される予定。

```Swift:TimeLineViewController.swift
var account: ACAccount?
var timeLine = [[String:AnyObject]]()
```

viewDidLoad に Twitter の認証系の処理を追加。
起動時に，設定した Twitter アカウントにアクセスしていいか聞く。
else は諸々エラー処理を書く。

```Swift:TimeLineViewController.swiftのviewDidLoad内
// Twitter の認証系
// インスタンス生成，タイプはtwitter
let accountStore = ACAccountStore()
let accountType = accountStore.accountTypeWithAccountTypeIdentifier(ACAccountTypeIdentifierTwitter)        accountStore.requestAccessToAccountsWithType(accountType, options: nil) { (granted, error) -> Void in
    if granted {
        let accounts = accountStore.accountsWithAccountType(accountType)
        if let account = accounts.first as? ACAccount {
                self.account = account
                self.getHomeTimeLine() //
        } else {
        }
    } else {
    }
}
```

アカウントの許可が出たら，Tweet を取りに行きます。
self.timeLine に全体情報が入ってきます。

```Swift:TimeLineViewController.swift
/*
 内部の更新
*/
func getHomeTimeLine() {
    // リクエストするURL
    let requestURL = NSURL(string: "https://api.twitter.com/1/statuses/home_timeline.json")
    // リクエストを作成
    let request = SLRequest(forServiceType: SLServiceTypeTwitter, requestMethod: .GET, URL: requestURL, parameters: nil)
    request.account = account
    request.performRequestWithHandler { (data, response, error) -> Void in
        let results = try! NSJSONSerialization.JSONObjectWithData(data, options: [])
        self.timeLine = results as! [[String: AnyObject]]
        dispatch_async(dispatch_get_main_queue()) {
            self.timeLineTableView.reloadData()
        }
    }
}
```

セルの数は取得したタイムラインの分でいいので timeLine の count にします。

```Swift:TimeLineViewController.swift
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

    return self.timeLine.count
}
```

cell の情報も取得した値を適用するので timeLine から必要な情報(ユーザアイコン，ユーザ名，ツイートの内容)を取り出す。このとき，ブレイクポイントを貼って timeLine の中身を見てみるといいかもです。対応するキーが決まっているのでそれを用いて取り出す。ちなみに当日は画像までは取りに行きませんでした。なるほど複雑だ・・・(do catch で書いてみました)

```Swift:TimeLineViewController.swift
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCellWithIdentifier("timeLineCell", forIndexPath: indexPath) as! TimeLineCell

    let tweet = timeLine[indexPath.row]
    let user = tweet["user"] as! [String: AnyObject]
    cell.useNameLabel.text = user["name"]! as? String
    cell.tweetTextView.text = tweet["text"] as! String

    let urlStr: String = user["profile_image_url"] as! String

    do {
        // アイコン画像の URL 指定
        let url: NSURL = NSURL(string: urlStr)!
        // NSData として取得
        let imageData: NSData = try NSData(contentsOfURL: url,
                                                 options: NSDataReadingOptions.DataReadingMappedIfSafe)
        // 取得した NSData を UIImage に変換
        cell.userIconImageView.image = UIImage(data: imageData)
    } catch {
         print("Exception")
    }
    return cell
}
```

ここまで実装すれば，設定したアカウントのツイート一覧が表示されるはずです。

![realmhandson002.png](https://qiita-image-store.s3.amazonaws.com/0/88266/e6c7292a-f500-b1a9-dab9-44d62ddcab64.png "realmhandson002.png")


## Tweet クラスを作ってモデルを切り出す
**コミット：e2d4061702cc430af516c086d91e062aa1e62c32**

MVC の考え方を少し。これくらいの実装ならこのままでもいい気がしますが，一応モデルを作って処理してました。

新規クラス(ファイル)を追加。
> New File... -> Cocoa Touch Class -> NSObject

![realmhandson001.png](https://qiita-image-store.s3.amazonaws.com/0/88266/79761a00-a1fb-8902-5e1d-b3fdfe43d781.png "realmhandson001.png")


プロパティを宣言して，取得した Twitter 情報を tweetDictionary で受けて必要な各値を取り出し，それぞれのプロパティに代入する。このクラスのプロパティを今後データとして使います。

```Swift:TweetModel.swift
import UIKit

class TweetModel: NSObject {

    // プロパティ
    var name = ""
    var text = ""
    var iconURL = ""

    convenience init(tweetDictionary: [String: AnyObject]) {
        self.init()

        let user = tweetDictionary["user"] as! [String: AnyObject]
        
        self.name = user["name"]! as! String
        self.iconURL = user["profile_image_url"] as! String
        self.text = tweetDictionary["text"] as! String
    }
}
```

TimeLineViewController.swift にこのモデルを使うように書き換えます。
**コミット：91bbd37cbb5c30065e71d864a1aeea352e53f1aa**

twitter の情報を格納するプロパティを変更する。

```Swift:TimeLineViewController.swift
// Array を宣言して初期化，TweetModel を使用
var timeLine = [TweetModel]()
```

getHomeTimeLine() 関数内を変更する。
timeLine をモデルの init 関数を使って必要な情報だけを含む Array にする。

```Swift:TimeLineViewController.swift
// 一旦全情報を含むデータを取得
let tweets = results as! [[String: AnyObject]]

// timeLine にはモデルを介して使用する情報のみを格納する
// TweetModel の convenience init で 取得した数の分のツイート情報(name, iconURL, text)が入る
self.timeLine = tweets.map {
    return TweetModel(tweetDictionary: $0)
}
```

ブレイクポイントで止めて見てみると下記のように必要な情報を含む Array になっています。Array の要素数は取得したツイート数。

![realmhandson003.png](https://qiita-image-store.s3.amazonaws.com/0/88266/80e3d362-34d5-388c-612a-bbc7b742223b.png "realmhandson003.png")

セルのプロパティに値を代入する部分も書き換える必要があります。

```Swift:TimeLineViewController.swift
// timeLine の indexPath.row に対応する値軍をモデルに展開
let tweet: TweetModel = timeLine[indexPath.row]

cell.useNameLabel.text = tweet.name
cell.tweetTextView.text = tweet.text
let urlStr: String = tweet.iconURL
// 画像表示は同じ処理なので省略
```


## Realm を使ってツイート情報を保存
**コミット：3202022ff7e168e3b93dbf29d0877571419fb007**

現状だと毎回最新 20 件しか取得できない。アプリを落とすとまた新しくデータを取りに行ってそれを表示するだけで当たり前だけど保存とかもできていない。起動時に Realm に保存できるように実装する。

TweetModel を Realm に対応する形に書き換える。

```Swift:TweetModel.swift
// 追加
import RealmSwift
```

```Swift:TweetModel.swiftのプロパティにdynamic修飾子をつける
// プロパティ(KVOの監視対象になるプロパティにはdynamicをつける)
dynamic var name = ""
dynamic var text = ""
dynamic var iconURL = ""
```

TimeLineViewController.swift 側にも RealmSwift を追加する。

```Swift:TimeLineViewController.swift
// 追加
import RealmSwift
```

Realm の結果を使うようにこちらも書き換える。

```Swift:TimeLineViewController.swiftのプロパティ部分
// Realmの結果を使うように書き換え
var timeLine: Results<TweetModel>?
// Realm用の通知トークン
var notificationToken: NotificationToken?
```


TimeLineViewController.swift の viewDidLoad に下記を追加。

```Swift:TimeLineViewController.swiftのviewDidLoad
// インスタンス生成
let realm = try! Realm()
self.timeLine = realm.objects(TweetModel)

// timeLine(オブジェクト)に変化があれば通知され実行される
self.notificationToken = timeLine?.addNotificationBlock({ (results, Error) -> () in
    // 更新するものはここに書く
    self.timeLineTableView.reloadData()
})
```

map を使って処理していた部分を削除して下記を追加。
realm にツイート情報を格納する。この際，モデルのプロパティ値が変化するため通知がされて timeLine が更新され，TableView の relaodData() が走る。


```Swift:TimeLineViewController.swiftのgetHomeTimeLine()内
// インスタンス生成
let realm = try! Realm()

// Realm に書き込み
try! realm.write({() -> Void in
    tweet.forEach {
        let tweetInfo = TweetModel(tweetDictionary: $0)
        // オブジェクトを追加
        realm.add(tweetInfo)
    }
})
```

あとエラーが 2 つ残ってるはず。キャストの問題なのでそれぞれ下記のようにする。

```Swift:TimeLineViewController.swift(?をつける)
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

    return self.timeLine?.count ?? 0
}
```

```Swift:TimeLineViewControllerのTableViewDataSource(cellForRowAtIndexPath)内(!をつける)
// timeLine の indexPath.row に対応する値軍をモデルに展開
let tweet: TweetModel = timeLine![indexPath.row]
```

ここで一度実行してみる。問題なく表示される。一度アプリを落としてまた起動してみる。(タイミングにもよるが)同じツイートが再度並んで 20 件-> 40 件表示されるはず。Realm にきちんと格納できているということだ。再度アプリを落として起動すると 60 件になる。


## 同じツイートは表示させないようにする
**コミット：c71c1251a8a8ae456af116d948e8a6160a94577e**


すでに表示済みのツイートがバッティングしているにもかかわらず，複数表示されているので，同じデータが Realm に入ってきた場合，それをマージしてしまう処理を追加します。Twitter の場合はツイートごとに id が振られているらしく，それを用いることで同じツイートかどうか判断するように実装します。

オブジェクトを追加する部分に **update:true** を追加します。 

```Swift:TimeLineViewController.swiftのgetHomeTimeLine()内
// Realm に書き込み
try! realm.write({() -> Void in
    tweet.forEach {
        let tweetInfo = TweetModel(tweetDictionary: $0)
        // オブジェクトを追加，バッティングした場合マージ
        realm.add(tweetInfo, update: true)
    }
})
```

id は取得するツイート情報と一緒に存在するのでそれを格納するプロパティを用意する。

```Swift:TweetModel.swift
// Tweetの重複を防ぐためのID(IDだとバッティングしない)
dynamic var tweetId = ""
```

ツイートを識別できる ID 相当のキーは幾つかあるそうだけど，"id_str" っていうのがいいらしい。init 関数内で取り出してプロパティに格納する。

```Swift:TweetModel.swiftのinit関数内
self.tweetId = tweetDictionary["id_str"] as! String
```

プライマリーキーを追加。ここではプライマリーキーとしてツイートの ID を指定して今後操作はこの ID を用いてできるようになる。プロパティを指定する。

```Swift:TweetModel.swift(新規追加)
override class func primaryKey() -> String? {
    return "tweetId"
}
```

ここで一度実行してみる。アプリが落ちます。Realm に新しい要素(tweetId)が追加されたけど・・・古いデータにはその要素を持っておらず，いわゆるマイグレーションが必要になる。後述するのでここは一旦アプリをアンインストールして再インストールしてみる。

先ほどと同じく起動して，アプリを落として再度起動してみる。先ほどはデータが 40 件に増えたが，今回は 20 件のままになっていると思う。同じツイートは表示されていないはず。ここで一度ツイートをしてみて再度同じことをやると・・・データは一番下に追加されているけど順番がソートされていない。追加しただけだから。

UIRefreshControl で最新ツイート取得，時間通りにソートする実装は発展編でやることにする。

## お気に入り(タブ 2)の実装
**コミット：7582f9775484579c1c65c05d4d207077b48db804**
**コミット；e7c2e75efe552af291b89542dc0b6ba1c552dfbc**

TimeLine の方で取得したツイート群をセルタップでお気に入り登録，解除するようにしてました。まずはツイートのセルをタップしたらお気に入り登録するように，セルのタップ部分(didSelectRowAtIndexPath)とモデルに追加実装します。

まずは，TweetModel にお気に入りの状態を格納するプロパティを用意します。

```Swift:TweetModel.swift
// お気に入り用プロパティ(Bool)
dynamic var favorited = false
```

次にセルタップでお気に入り登録，解除できるように追加実装します。
TableViewDelegate メソッドは GitHub では用意していますので適宜お使いください。

```Swift:TimeLineViewController.swift
// MARK: - TableView Delegate Method

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

    let tweet = self.timeLine![indexPath.row]

    // インスタンス生成
    let realm = try! Realm()

    // セルをタップするとお気に入りにする
    // もう一度タップすると気に入り解除
    // realm に書き込む
    try! realm.write { () -> Void in
        // お気に入りかそうでないか
        tweet.favorited = !tweet.favorited
    }
}
```

ここで実行すると，新しく追加したプロパティが問題になります。
マイグレーションについては次回考えてみます。

## お気に入り画面の方の実装
**コミット：5b00e0361ba54ee0a069780182cc7fbb2f629a16**

基本的にタイムラインの実装と同じでいいのですが，Twitter のアカウント認証系の処理を省けます。お気に入り用の Array を用意してタイムラインと同様に実装します。

FavoriteViewController.swift に追加実装していきます。RealmSwift を import してタイムラインの方と同様に二つのプロパティを追加します。

```Swift:FavoriteViewController.swift
// 追加
import RealmSwift
```

```Swift:FavoriteViewController.swift
// プロパティ追加
var favoritesTweet: Results<TweetModel>?
var notificationToken: NotificationToken?
```

favoriteTweet にはお気に入りのフラグがたったツイート情報が入るようにする。

```Swift:FavoriteViewController.swiftのviewDidLoad
// インスタンス生成
let realm = try! Realm()

// 検索条件を追加(お気に入りがTrueのときのものを取得)
self.favoritesTweet = realm.objects(TweetModel).filter("favorited = true")

// こいつが変わったら通知するトークン
notificationToken = self.favoritesTweet?.addNotificationBlock({ (Results, Error) -> () in
    // 更新するものはここに書く
    self.favoritesTableView.reloadData()
})
```

セルの数は，favoriteTweet の要素数と同じ。

```Swift:FavoriteViewController.swiftのTableViewDataSourceのメソッド
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

    return self.favoritesTweet?.count ?? 0
}
```

セルを返す部分でお気に入りのツイートをセルに表示するようにタイムラインの方と同様の実装をする。

```Swift:FavoriteViewController.swift
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCellWithIdentifier("favoriteCell", forIndexPath: indexPath) as! FavoriteCell

    // お気に入りのツイート indexPath.row に対応する値軍をモデルに展開
    // indexPath.row に対応する値軍をモデルに展開
    let tweet: TweetModel = self.favoritesTweet![indexPath.row]

    cell.userNameLabel.text = tweet.name
    cell.tweetTextView.text = tweet.text
    let urlStr: String = tweet.iconURL

    do {
        // アイコン画像の URL 指定
        let url: NSURL = NSURL(string: urlStr)!
        // NSData として取得
        let imageData: NSData = try NSData(contentsOfURL: url,
                                               options: NSDataReadingOptions.DataReadingMappedIfSafe)
        // 取得した NSData を UIImage に変換
        cell.userIconImageView.image = UIImage(data: imageData)
    } catch {
        print("Exception")
    }
    return cell
}
```

マイグレーションが必要なので，一旦アプリを削除してもう一度ビルドしてみてください。タイムラインのツイートのセルをタップしてみると・・・セルの色が変わるだけ・・・その後，お気に入りタブを押してみるとお気に入りしたツイートが表示されているはず。

![realmhandson004.png](https://qiita-image-store.s3.amazonaws.com/0/88266/2e4cfd3a-83b0-b4dd-64a7-6356262de926.png "realmhandson004.png")

マイグレーションも少しあったんだけど，もう少し見てみたいので分けてみます。最近なんかアップデートされていましたね。これでだいたいハンズオンの内容はできたと思います。実際はツイッターの取得部分まででかなり時間を使って Realm の実装時間はあまりなかった感じです。なのでツイートを表示する TableView 作成部分まで Github で用意した感じです。最初の部分でわからないことがあれば聞いてください。

## 少し修正(Xcode 7.3.2)
検証してるときにXcode 7.3.2 の方で実機ビルド時にエラーが出るところがあったので該当箇所の修正をします。でなければ無視してください。

クロージャのエラーが出ていたので修正。

```Swift:TimeLineViewController.swiftのviewDidLoad
// timeLine(オブジェクト)に変化があれば通知され実行される
self.notificationToken = self.timeLine?.addNotificationBlock { results in
    // 更新するものはここに書く
    self.timeLineTableView.reloadData()
}
```

```Swift:FavoriteViewController.swiftのviewDidLoad
// こいつが変わったら通知するトークン
notificationToken = self.favoritesTweet?.addNotificationBlock { results in
    // 更新するものはここに書く
    self.favoritesTableView.reloadData()
}
```


## 次回(予定)
* UIRefreshControl(引っ張って更新)で最新ツイート取得
* タイムラインのソート(時間順)
* マイグレーション(最近なんか良くなったと聞いた)
* 他あれば

## 参考
[Realm 公式サイト](https://realm.io/jp/)