読者です 読者をやめる 読者になる 読者になる

【swift】RealmSwiftとObjectMapperでローカルDBを使う方法メモ

swift ios iPhoneアプリ開発

はじめに

今回作りたかったものは辞書アプリみたいなイメージで
書籍のカテゴリーが一覧表示されていて選択すると紐づく書籍がリスト表示されるだけ

やりたかったことは以下
・辞書データはサーバーに保存
・できるだけオフラインでも使えるようにしたい

で、仕組み的には以下
・アプリ起動時にサーバーからデータを取得してローカルdbに保存
・初回は全データ取得、2回目以降は差分データ取得
・各ViewControllerとかでデータを参照したいときはapiを叩くのでなくローカルdbを参照する

で、ローカルdbは使ったことなかったので調べてみるとRealmというのが1番ヒットした気がしたのでこれを使って見る
ObjectMapperもついでに使って見る

データベース構成

BookCategory(書籍カテゴリーテーブル)

項目カラム備考
idカテゴリーidPK
nameカテゴリー名
updated更新日時
deleted削除日時

Book(書籍テーブル)

項目カラム備考
id書籍idPK
category_id書籍カテゴリーidFK
name書籍名
updated更新日時
deleted削除日時

必要なライブラリをインストール

・RealmSwift
・ObjectMapper
・AFNetworking(apiからデータを取得するのに使用)

Podfile

use_frameworks!

target 'SampleRealm' do
  pod 'AFNetworking'
  pod 'RealmSwift'
  pod 'ObjectMapper'
end

target 'SampleRealmTests' do
  pod 'AFNetworking'
  pod 'RealmSwift'
  pod 'ObjectMapper'
end

target 'SampleRealmUITests' do
end

インストール

pod install

実装

ローカルdbを作る

といってもクラスを用意するだけ

BookCategory.swift

import Foundation
import RealmSwift
import ObjectMapper

class BookCategory: Object, Mappable {
    
    dynamic var id = 0
    dynamic var name = ""
    
    override static func primaryKey() -> String? {
        return "id"
    }
    
    required convenience init?(_ map: Map) {
        self.init()
        mapping(map)
    }

    func mapping(map: Map) {
        id <- map["id"]
        name <- map["name"]
    }
}

Book.swift

import Foundation
import RealmSwift
import ObjectMapper

class Book: Object, Mappable {
    dynamic var id = 0
    dynamic var category_id = 0
    dynamic var name = ""
    
    override static func primaryKey() -> String? {
        return "id"
    }
    
    required convenience init?(_ map: Map) {
        self.init()
        mapping(map)
    }
    
    func mapping(map: Map) {
        id <- map["id"]
        category_id <- map["category_id"]
        name <- map["name"]
    }
}
サーバーからデータを取得してローカルdbに反映

今回は取得したデータをテーブルビューにした。それるのでテーブルビュー自体の話はなしで

ViewController.swift

import UIKit
import RealmSwift
import AFNetworking
import ObjectMapper

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    var realm: Realm! = nil

    ・・・

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        // Realm初期化
        realm = try! Realm()
        
        // データ初期化
        initializeData()

        ・・・
    }

    // サーバーから取得したデータをローカルDBに反映
    func initializeData() {
        
        var urlPath = "http://example.com/api/get_mst.php"
        let parameters: String? = nil
        let manager :AFHTTPSessionManager = AFHTTPSessionManager()
        
        // 前回データ更新日時をUserDefaultsから取得
        // apiにこの日付を送って必要な差分データを返してもらう
        let userDefaults = NSUserDefaults.standardUserDefaults()
        if let previousDbUpdateTime = userDefaults.objectForKey("dbUpdateTime") as? NSDate {
            let dateFormatter: NSDateFormatter = NSDateFormatter()
            dateFormatter.locale = NSLocale(localeIdentifier: "ja")
            dateFormatter.dateFormat = "yyyyMMddHHmmss"
            urlPath += "?previousDbUpdateTime=" + dateFormatter.stringFromDate(previousDbUpdateTime)
        }
        
        manager.GET(urlPath, parameters: parameters, progress: { (progress) in
            // progress
            }, success: { (task, response) in
                if response!["status"] as! Int == 1 {
                    if let result = response!["result"] as? Dictionary<String,AnyObject> {

                        // データ登録・更新
                        if let resultRegist = result["regist"] as? Dictionary<String,AnyObject> {
                            // カテゴリーリスト
                            let bookCategory = Mapper<BookCategory>().mapArray(resultRegist["bookCategory"] as? [AnyObject])
                            for v in bookCategory! {
                                try! self.realm.write {
                                    self.realm.add(v, update: true) // ★ローカルdb更新(同じidのレコードがなければ新規登録、あれば更新)
                                }
                            }
                            // 書籍カテゴリーリスト
                            let book = Mapper<Book>().mapArray(resultRegist["book"] as? [AnyObject])
                            for v in book! {
                                try! self.realm.write {
                                    self.realm.add(v, update: true) // ★ローカルdb更新(同じidのレコードがなければ新規登録、あれば更新)
                                }
                            }
                        }
                        
                        // データ削除
                        if let resultDelete = result["delete"] as? Dictionary<String,AnyObject> {
                            if let bookCategoryIds = resultDelete["bookCategory"] as? [Int] {
                                for bookCategoryId in bookCategoryIds {
                                    let bookCategoryResult = self.realm.objects(BookCategory).filter("id = " + bookCategoryId.description)
                                    if 0 < bookCategoryResult.count {
                                        try! self.realm.write {
                                            self.realm.delete(bookCategoryResult[0]) // ★ローカルdbからレコード削除
                                        }
                                    }
                                }
                            }
                        }

                        // ★データ取得完了したらテーブルビューの中身を再描画
                        self.tableView.reloadData()
                    }
                    
                    // ローカルdb更新日時を保存
                    userDefaults.setObject(NSDate(), forKey: "dbUpdateTime")

                }
            }) { (task, error) in
                //
        }
    }
}
サーバー側実装

といっても、前回更新日時を受け取ってdbの内容をjsonで返すだけなので中身だけ書いておく

// パラメータで受け取った日時とdbの更新日時、削除日時を見てjsonを返す

$json = array(
    "status" => 1,
    "result" => array(
        // 登録または更新データを各テーブル分返す
        "regist" => array(
            "bookCategory" => array(
                 array("id"=>1, "name"=>"カテゴリー1")
                 array("id"=>2, "name"=>"カテゴリー2")
                 array("id"=>3, "name"=>"カテゴリー3")
            ),
            "book" => array(
                 array("id"=>1, "cateogory_id" => 1, "name"=>"書籍1")
                 array("id"=>2, "cateogory_id" => 1, "name"=>"書籍2")
                 array("id"=>3, "cateogory_id" => 1, "name"=>"書籍3")
            ),
        ),

        // 削除するidのリストを各テーブル分返す
        "delete" => array(
            "bookCateogryIds" => array(4,5,6),
        ),
    ),
);
ローカルdbの内容をテーブルビューに表示

ViewController.swift

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    ・・・

    // テーブルの行数をかえします
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return realm.objects(BookCategory).count
    }
    
    // テーブルセルにデータをセットします
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let bookCategoryResult = realm.objects(BookCategory)
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
        cell.textLabel?.text = bookCategoryResult[indexPath.row].name
        return cell
    }

    // テーブルセルを選択されたら詳細画面へカテゴリーIDを渡す
    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        let bookCategoryResult = realm.objects(BookCategory)
        let viewController: DetailViewController = DetailViewController()
        viewController.category_id = bookCategoryResult[indexPath.row].id
        self.navigationController?.pushViewController(viewController, animated: true)
    }

    ・・・
}

さいごに

とりあえずこんな感じでやりたいことはできました。
オフラインでも使えるようにって書いたのに起動時にオフラインの場合に対応できてないけど・・・

あと、android版もあるみたいなのでandroidで今度同じことをやってみたいと思います

以上です