はじめに
今年の 3 月中旬にあった iOS の Realm のハンズオンの復習をボチボチやらないとーということで書いてみました。まだ理解が乏しいところもあるので曖昧なところもあると思いますが,ご指摘いただければと思います。ごちゃごちゃ書いてみにくい記事になりました。申し訳ありません。行きたかったけど行けなかった方,少しやってみようと思っている方の役に立てばいいなー。
あの日は結構準備段階で時間を使って,Realm を触る時間が少なくなっていたのである程度作ったやつがあったらよかったのかなーと思いました。
勉強会レポ(概要)書いてます。よろしければどうぞ。
【勉強会レポ160312008】Realm ハンズオン(iOS) #1
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 ブランチをチェックアウトして動作確認してみてください。
ここまでの実装で気になる方は master ブランチのコミット履歴をごらんください。
TimeLine と Favorites Tweet が 2 つのタブに適用されているはずです。
$ 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 が出たのでそれでいいです。
$ cd RealmHandsOnSample/
$ git checkout master
$ pod install
他にも直接ドラッグアンドドロップで導入する方法と,Carthage(カルタゴ)で導入する方法があるようです。
公式サイト参照してください。
https://realm.io/docs/swift/latest/
この後の実装はブランチを分けています。 feature/twitter-realm で実装していますので参考にしてください。Github で diff(差分) を見るのがいいと思います。
Realm が使えるか確かめてみる
コミット:e4887d415febe121cca1cd38bd9bcc31d49635a0
AppDelegate.swift に RealmSwift を import して,一番上のメソッド内に下記のように書いてみる。
import RealmSwift
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)
Twitter 部分の実装
コミット:3fef2110fde74bb44b031057dd680d2b474125b0
ここは Realm とは直接関係ないのですが,認証系とツイートを取得するとこやります。
TimeLineViewController.swift に下記の 2 つを import します。
import Accounts
import Social
下記プロパティを追加。timeLine に twitter 情報が格納される予定。
var account: ACAccount?
var timeLine = [[String:AnyObject]]()
viewDidLoad に Twitter の認証系の処理を追加。
起動時に,設定した Twitter アカウントにアクセスしていいか聞く。
else は諸々エラー処理を書く。
// 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 に全体情報が入ってきます。
/*
内部の更新
*/
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 にします。
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.timeLine.count
}
cell の情報も取得した値を適用するので timeLine から必要な情報(ユーザアイコン,ユーザ名,ツイートの内容)を取り出す。このとき,ブレイクポイントを貼って timeLine の中身を見てみるといいかもです。対応するキーが決まっているのでそれを用いて取り出す。ちなみに当日は画像までは取りに行きませんでした。なるほど複雑だ・・・(do catch で書いてみました)
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
}
ここまで実装すれば,設定したアカウントのツイート一覧が表示されるはずです。
Tweet クラスを作ってモデルを切り出す
コミット:e2d4061702cc430af516c086d91e062aa1e62c32
MVC の考え方を少し。これくらいの実装ならこのままでもいい気がしますが,一応モデルを作って処理してました。
新規クラス(ファイル)を追加。
New File... -> Cocoa Touch Class -> NSObject
プロパティを宣言して,取得した Twitter 情報を tweetDictionary で受けて必要な各値を取り出し,それぞれのプロパティに代入する。このクラスのプロパティを今後データとして使います。
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 の情報を格納するプロパティを変更する。
// Array を宣言して初期化,TweetModel を使用
var timeLine = [TweetModel]()
getHomeTimeLine() 関数内を変更する。
timeLine をモデルの init 関数を使って必要な情報だけを含む Array にする。
// 一旦全情報を含むデータを取得
let tweets = results as! [[String: AnyObject]]
// timeLine にはモデルを介して使用する情報のみを格納する
// TweetModel の convenience init で 取得した数の分のツイート情報(name, iconURL, text)が入る
self.timeLine = tweets.map {
return TweetModel(tweetDictionary: $0)
}
ブレイクポイントで止めて見てみると下記のように必要な情報を含む Array になっています。Array の要素数は取得したツイート数。
セルのプロパティに値を代入する部分も書き換える必要があります。
// 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 に対応する形に書き換える。
// 追加
import RealmSwift
// プロパティ(KVOの監視対象になるプロパティにはdynamicをつける)
dynamic var name = ""
dynamic var text = ""
dynamic var iconURL = ""
TimeLineViewController.swift 側にも RealmSwift を追加する。
// 追加
import RealmSwift
Realm の結果を使うようにこちらも書き換える。
// Realmの結果を使うように書き換え
var timeLine: Results<TweetModel>?
// Realm用の通知トークン
var notificationToken: NotificationToken?
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() が走る。
// インスタンス生成
let realm = try! Realm()
// Realm に書き込み
try! realm.write({() -> Void in
tweet.forEach {
let tweetInfo = TweetModel(tweetDictionary: $0)
// オブジェクトを追加
realm.add(tweetInfo)
}
})
あとエラーが 2 つ残ってるはず。キャストの問題なのでそれぞれ下記のようにする。
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.timeLine?.count ?? 0
}
// timeLine の indexPath.row に対応する値軍をモデルに展開
let tweet: TweetModel = timeLine![indexPath.row]
ここで一度実行してみる。問題なく表示される。一度アプリを落としてまた起動してみる。(タイミングにもよるが)同じツイートが再度並んで 20 件-> 40 件表示されるはず。Realm にきちんと格納できているということだ。再度アプリを落として起動すると 60 件になる。
同じツイートは表示させないようにする
コミット:c71c1251a8a8ae456af116d948e8a6160a94577e
すでに表示済みのツイートがバッティングしているにもかかわらず,複数表示されているので,同じデータが Realm に入ってきた場合,それをマージしてしまう処理を追加します。Twitter の場合はツイートごとに id が振られているらしく,それを用いることで同じツイートかどうか判断するように実装します。
オブジェクトを追加する部分に update:true を追加します。
// Realm に書き込み
try! realm.write({() -> Void in
tweet.forEach {
let tweetInfo = TweetModel(tweetDictionary: $0)
// オブジェクトを追加,バッティングした場合マージ
realm.add(tweetInfo, update: true)
}
})
id は取得するツイート情報と一緒に存在するのでそれを格納するプロパティを用意する。
// Tweetの重複を防ぐためのID(IDだとバッティングしない)
dynamic var tweetId = ""
ツイートを識別できる ID 相当のキーは幾つかあるそうだけど,"id_str" っていうのがいいらしい。init 関数内で取り出してプロパティに格納する。
self.tweetId = tweetDictionary["id_str"] as! String
プライマリーキーを追加。ここではプライマリーキーとしてツイートの ID を指定して今後操作はこの ID を用いてできるようになる。プロパティを指定する。
override class func primaryKey() -> String? {
return "tweetId"
}
ここで一度実行してみる。アプリが落ちます。Realm に新しい要素(tweetId)が追加されたけど・・・古いデータにはその要素を持っておらず,いわゆるマイグレーションが必要になる。後述するのでここは一旦アプリをアンインストールして再インストールしてみる。
先ほどと同じく起動して,アプリを落として再度起動してみる。先ほどはデータが 40 件に増えたが,今回は 20 件のままになっていると思う。同じツイートは表示されていないはず。ここで一度ツイートをしてみて再度同じことをやると・・・データは一番下に追加されているけど順番がソートされていない。追加しただけだから。
UIRefreshControl で最新ツイート取得,時間通りにソートする実装は発展編でやることにする。
お気に入り(タブ 2)の実装
コミット:7582f9775484579c1c65c05d4d207077b48db804
コミット;e7c2e75efe552af291b89542dc0b6ba1c552dfbc
TimeLine の方で取得したツイート群をセルタップでお気に入り登録,解除するようにしてました。まずはツイートのセルをタップしたらお気に入り登録するように,セルのタップ部分(didSelectRowAtIndexPath)とモデルに追加実装します。
まずは,TweetModel にお気に入りの状態を格納するプロパティを用意します。
// お気に入り用プロパティ(Bool)
dynamic var favorited = false
次にセルタップでお気に入り登録,解除できるように追加実装します。
TableViewDelegate メソッドは GitHub では用意していますので適宜お使いください。
// 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 してタイムラインの方と同様に二つのプロパティを追加します。
// 追加
import RealmSwift
// プロパティ追加
var favoritesTweet: Results<TweetModel>?
var notificationToken: NotificationToken?
favoriteTweet にはお気に入りのフラグがたったツイート情報が入るようにする。
// インスタンス生成
let realm = try! Realm()
// 検索条件を追加(お気に入りがTrueのときのものを取得)
self.favoritesTweet = realm.objects(TweetModel).filter("favorited = true")
// こいつが変わったら通知するトークン
notificationToken = self.favoritesTweet?.addNotificationBlock({ (Results, Error) -> () in
// 更新するものはここに書く
self.favoritesTableView.reloadData()
})
セルの数は,favoriteTweet の要素数と同じ。
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.favoritesTweet?.count ?? 0
}
セルを返す部分でお気に入りのツイートをセルに表示するようにタイムラインの方と同様の実装をする。
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
}
マイグレーションが必要なので,一旦アプリを削除してもう一度ビルドしてみてください。タイムラインのツイートのセルをタップしてみると・・・セルの色が変わるだけ・・・その後,お気に入りタブを押してみるとお気に入りしたツイートが表示されているはず。
マイグレーションも少しあったんだけど,もう少し見てみたいので分けてみます。最近なんかアップデートされていましたね。これでだいたいハンズオンの内容はできたと思います。実際はツイッターの取得部分まででかなり時間を使って Realm の実装時間はあまりなかった感じです。なのでツイートを表示する TableView 作成部分まで Github で用意した感じです。最初の部分でわからないことがあれば聞いてください。
少し修正(Xcode 7.3.2)
検証してるときにXcode 7.3.2 の方で実機ビルド時にエラーが出るところがあったので該当箇所の修正をします。でなければ無視してください。
クロージャのエラーが出ていたので修正。
// timeLine(オブジェクト)に変化があれば通知され実行される
self.notificationToken = self.timeLine?.addNotificationBlock { results in
// 更新するものはここに書く
self.timeLineTableView.reloadData()
}
// こいつが変わったら通知するトークン
notificationToken = self.favoritesTweet?.addNotificationBlock { results in
// 更新するものはここに書く
self.favoritesTableView.reloadData()
}
次回(予定)
- UIRefreshControl(引っ張って更新)で最新ツイート取得
- タイムラインのソート(時間順)
- マイグレーション(最近なんか良くなったと聞いた)
- 他あれば