Swift
Realm

Swift + Alamofire + ObjectMapper + Realm [Swift3対応]

More than 1 year has passed since last update.

そろそろSwiftを社内の案件に導入しようか検討中です。
受託案件ではほぼ必須機能と言える

  1. ネットワーク経由でJSON取得
  2. JSONをパースしてDBにinsert
  3. DBの値をTableViewに表示

という流れ。
せっかくナウな言語であるSwiftなんだから、イケてるライブラリでキメキメなコーディングをしたい!
ということで、Swiftや新しいライブラリの調査がてら、ざっくりとしたサンプルを作ってみました。

サンプル

サンプルをこちらにあげてみました
Github - SatoshiN21/RealmSample
※ざっくりとSwift3に対応しております。

今回ははてブのITカテゴリーのホットエントリーRSSをJSON形式で取得しています。
ナウなMaterial DesignのCardViewっぽい見た目にしてみました。

iOS Simulator Screen Shot 2015.05.14 19.13.05.png

1. JSON取得

Alamofireではてブのフィード情報を取得しています。
ホットエントリーはRSS形式で配布されていたので、googleのAPIを用いてJSON形式に変換しました。

import Alamofire
import SwiftyJSON

//  google JSON api + hatena bookmark hotentry
let hotEntryUrl = "https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&q=http://b.hatena.ne.jp/hotentry/it.rss&num=100"

// Alamofireを用いてGETリクエスト
Alamofire.request(hotEntryUrl).responseJSON { (response) in

    guard response.result.isSuccess, let value = response.result.value else {

        // FIXME:便宜上こちら無視してますが、実装時はエラーのハンドリングを行う必要があります
        return
    }

    // SwiftyJSON方式
    let json = JSON(value)

取得したJSONをSwiftyJSON形式に変換して持ち直します。
今回のサンプルではリクエストエラー時の処理は実装していないので、気をつけてください!

2. JSONをパースしてDBにinsert

SwiftyJSON形式にすると、パース作業が捗りますね!チェーン状に目的のエレメントを取得することができます。
エントリー一覧を取得します。

let json = JSON(responseObject!)
let entries = json["responseData"]["feed"]["entries"]

ObjectMapperを用いてEntryオブジェクトにマッピングしています。
ObjectMapperとRealmを共存させる方法はgologo13さんの投稿を参考にさせて頂きました。

import ObjectMapper

realm.beginWrite()

// JSONをEntryオブジェクトにマッピング
for (_, subJson) in entries {
    if let entry = Mapper<Entry>().map(JSONObject: subJson.dictionaryObject) {
        realm.add(entry, update: true)
    }
}

realm.commitWrite()

RealmSwift.Objectを継承したEntryオブジェクト内の仕様はほぼ参考にさせて頂いた記事と同様ですが、
公開日の日付を独自定義したDateTransformでDateに変換しています。

extension Entry : Mappable {

    func mapping(map: Map) {
        content         <- map["content"]
        link            <- map["link"]
        publishedDate   <- (map["publishedDate"] , EntryDateTransform())
        title           <- map["title"]
        contentSnippet  <- map["contentSnippet"]
    }
}

// 独自定義したDateTransform
// フォーマットとNSLocaleを指定してStringをNSDateに変換
class EntryDateTransform : DateTransform {
    override func transformFromJSON(_ value: Any?) -> Date? {
        if let dateStr = value as? String {
            return Date.dateWithString(
                dateStr,
                format: "E, dd MMM yyyy HH:mm:ss zzzz" ,
                locale : Locale(identifier: "en_US"))
        }
        return nil
    }
}

extension Date {
    public static func dateWithString(_ dateStr : String? , format : String, locale : Locale) ->Date? {

        guard let dateStr = dateStr else {
            return nil
        }
        let df : DateFormatter = DateFormatter()
        df.locale = Locale(identifier: "en_US")
        df.timeZone = TimeZone.current
        df.dateFormat = format
        return df.date(from: dateStr)
    }
}

3. DBの値をTableViewに表示

Realmの更新が発生したタイミングでUITableView#reloadData()を実行し、UITableViewの表示を更新。
RealmSwift.Objectを継承したクラスでobjects(type: Object.Type)を呼び出すと、DBに保存されている全ての値が取得できます。

    func updateTableView() {

        do {
            self.entries = try Realm().objects(Entry.self).sorted(by: { (entry1, entry2) -> Bool in
            let res = entry1.publishedDate.compare(entry2.publishedDate)
            return (res == .orderedAscending || res == .orderedSame)
            })
        }catch {}

        tableView.reloadData()
    }

    // MARK:- UITableView DataSource / Delegate
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if let entries = entries {
            return entries.count
        }
        return 0
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell  = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier") as! EntryTableViewCell

        // if entries have been nil,"cellForRowAtIndexPath:indexPath:" isn't called.
        let entry = entries![indexPath.row]

        // date format.
        let df = DateFormatter()
        df.locale = Locale(identifier: "ja_JP")
        df.timeZone = TimeZone.current
        df.dateFormat = "MM/dd"
        let dateStr = df.string(from: entry.publishedDate as Date)

        cell.titleLabel.text = [dateStr,entry.title].joined(separator: " ")
        cell.descriptionLabel.text = entry.contentSnippet

        return cell
    }

以上

かなりざっくりとではありますが、JSONを取得し、内部DBに保存/表示の方向性は見えたかなという感じです。
Objective-Cの時はCoreDataを使っていましたが、データが肥大化するほどみるみるパフォーマンスが落ちていく様に呆然としていた記憶があります。
Realmを使ってどれほどパフォーマンスに差が出るのか、いまから楽しみです。

参考にさせて頂いた記事

[Swift]ObjectMapperを使ったRealmモデルへのJSONマッピング - Qiita
情弱がSwift用のObjectMapper読んでみた - Qiita