【swift】RealmSwiftとObjectMapperでローカルDBを使う方法メモ
はじめに
今回作りたかったものは辞書アプリみたいなイメージで
書籍のカテゴリーが一覧表示されていて選択すると紐づく書籍がリスト表示されるだけ
やりたかったことは以下
・辞書データはサーバーに保存
・できるだけオフラインでも使えるようにしたい
で、仕組み的には以下
・アプリ起動時にサーバーからデータを取得してローカルdbに保存
・初回は全データ取得、2回目以降は差分データ取得
・各ViewControllerとかでデータを参照したいときはapiを叩くのでなくローカルdbを参照する
で、ローカルdbは使ったことなかったので調べてみるとRealmというのが1番ヒットした気がしたのでこれを使って見る
ObjectMapperもついでに使って見る
データベース構成
BookCategory(書籍カテゴリーテーブル)
項目 | カラム | 備考 |
---|---|---|
id | カテゴリーid | PK |
name | カテゴリー名 | |
updated | 更新日時 | |
deleted | 削除日時 |
Book(書籍テーブル)
項目 | カラム | 備考 |
---|---|---|
id | 書籍id | PK |
category_id | 書籍カテゴリーid | FK |
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) } ・・・ }