はじめに
みなさん、こんにちは。株式会社NexceedでiOSエンジニアをしている@cottpanです😎
アプリの開発を行う中で、データベースの操作は避けて通れない操作だと思います。
- APIを叩いてJSONをパース
- パースしたデータを、アプリのDBに保存
- JSONエンコーディングして、データ送信
この記事では、これらの操作をお手軽に実現でき、パフォーマンスもそこそこ良い、Realm、Codableを使った方法を説明したいと思います。
Realm
Realmとは、Core DataやSQLiteといった従来のDBより、高速で使いやすいといった特徴がある、モバイル向けに開発されたDBです。
Realm: Create reactive mobile apps in a fraction of the time
インストール方法はこの記事では割愛しますが、CocoaPodsでインストール出来ます。
Codable
Swift4から新たにFoundationに追加され、簡単にJSONのデコード、エンコードを行うことができる仕組みが提供されました。これを使うことで、他のフレームワークなしに、複雑なJSONも一発で変換できます!
まずやってみる
今回は以下のようなJSONデータを利用するとします。
{
"name": "hogehoge",
"id": 553,
}
モデルの作成
このデータを格納するための、モデルクラスを作成します。Codableに準拠させるので、JSONパースができ、そのままRealmのテーブルを作るためのクラスとしても使用できます。
import Foundation
import RealmSwift
class UserData: Object, Codable {
// カラム定義
@objc dynamic var name: String = ""
@objc dynamic var id: Int = 0
// プライマリキーの定義
override public static func primaryKey() -> String? {
return "id"
}
}
エンコード、デコードメソッドの実装は、自動で行ってくれるので完成です。
これで準備は整いました。あとはJSON→Realmの作業をやってみましょう。
実行
// Realmのインスタンスを準備
let realm = try! Realm()
let dataStr = """
{
"name": "hogehoge",
"id": 553,
}
"""
let data = dataStr.data(using: .utf8)!
// JSONをUserDataクラスのオブジェクトにパース
let obj = try! JSONDecoder().decode(UserData.self, from: data)
// Realmに書き込み
try! realm.write {
realm.add(obj)
}
これでRealmにデータを格納することが出来ました。とっても簡単ですね😀
アプリのDocuments直下にデータベースの実体があるので確認してみましょう。拡張子が.realm
になっているものがデータベースです。このファイルはMac App Storeで配布されているRealm Browserを使用することで確認できます。
ちゃんと追加できることが確認できました!
今度はRealmからデータを取得し、JSON形式として出力してみます!
let obj = realm.object(ofType: UserData.self, forPrimaryKey: 553)
let encoder = JSONEncoder()
let data = try! encoder.encode(obj)
let jsonStr = String(data: data, encoding: .utf8)!
print(jsonStr)
XcodeのコンソールにJSON形式の文字列が出力されました😃
ポイント
変数
変数は@objc dynamic
属性を定義する必要があります。またInt
,Bool
,Float
,Double
型に関してはオプショナルにする場合、RealmOptional<Type>
として宣言する必要があります。宣言できる型は以下の表を参考にしてください。
Type | Non-optional | Optional |
---|---|---|
Bool | @objc dynamic var value = false |
let value = RealmOptional<Bool>() |
Int | @objc dynamic var value = 0 |
let value = RealmOptional<Int>() |
Float | @objc dynamic var value: Float = 0.0 |
let value = RealmOptional<Float>() |
Double | @objc dynamic var value: Double = 0.0 |
let value = RealmOptional<Double>() |
String | @objc dynamic var value = "" |
@objc dynamic var value: String? = nil |
Data | @objc dynamic var value = Data() |
@objc dynamic var value: Data? = nil |
Date | @objc dynamic var value = Date() |
@objc dynamic var value: Date? = nil |
Object | n/a: must be optional | @objc dynamic var value: Class? |
List | let value = List<Type>() |
n/a: must be non-optional |
LinkingObjects | let value = LinkingObjects(fromType: Class.self, property: "property") |
n/a: must be non-optional |
(Realm -> Property cheatsheetより引用)
自動Codable準拠
String, Int, Double, Date, Data, URLなどは既にCodableに準拠しているため、明示的にencode,decodeメソッドを実装する必要はありません。しかし、Date型やオプショナル型などそのままでは使えない場合があるので、自分でencode,decodeメソッドを実装する必要があります。
応用編
様々な型のデータ型に対応できるように、encode,decodeメソッドを自前で実装する場合をまとめます。
{
"id": 553,
"name": "hogehoge",
"birthday": "1999-01-02T10:45:22+09:00",
"favorite_food": "apple",
"is_from_japan": true,
"favorite_song": null,
"age": null
}
先程のJSONを拡張して、データを追加してみましょう。Date型、NOT NULLの変数も含まれています。
このデータを読み込めるようにUserDataクラスを編集します。
class UserData: Object, Codable {
// カラム定義
@objc dynamic var id: Int = 0
@objc dynamic var name: String = ""
@objc dynamic var birthday: Date = Date()
@objc dynamic var isFromJapan: Bool = false
@objc dynamic var favoriteSong: String?
let age = RealmOptional<Int>()
// プライマリーキーの定義
override public static func primaryKey() -> String? {
return "id"
}
private enum CodingKeys: String, CodingKey {
case id
case name
case birthday
case isFromJapan = "is_from_japan"
case favoriteSong = "favorite_song"
case age
}
required convenience public init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
isFromJapan = try container.decode(Bool.self, forKey: .isFromJapan)
favoriteSong = try container.decodeIfPresent(String.self, forKey: .favoriteSong)
age.value = try container.decodeIfPresent(Int.self, forKey: .age)
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
let birthdayStr = try container.decode(String.self, forKey: .birthday)
birthday = dateFormatter.date(from: birthdayStr)!
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(isFromJapan, forKey: .isFromJapan)
try container.encode(favoriteSong, forKey: .favoriteSong)
try container.encode(age.value, forKey: .age)
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
dateFormatter.timeZone = TimeZone.current
let bitrhdayStr = dateFormatter.string(from: birthday)
try container.encode(bitrhdayStr, forKey: .birthday)
}
}
先ほどと比べてかなり長くなってしまいましたが、追加した箇所を説明してみます。
CodingKey
JSONのキーと、Swiftクラスの変数名が異なる場合にはCodingKeyを定義することで、読み込むことが出来ます。
init(decode)メソッド
変数ごとに、型を指定してデコードします。
- non-optionalならば、
decode
メソッド - optionalならば、
decodeIfPresent
メソッド -
RealmOptional<Type>
ならば、[variable].value
に対して、decodeIfPresent
メソッド
また、convenienceイニシャライザを使用するので、オプショナルでない変数に対しては、初期値を適当に設定してください。日付に関しては、ISO8601拡張形式でのDateを読み込むため、DateFormatterを使用して、明示的に形式を指定しています。
encodeメソッド
decodeメソッドの逆を行っているだけなので、行数は多いですが、わかりやすかと思います。
Date型に関しては、Dateformatterを用いて、Stringに一度変換してからencodeを行っています。
さいごに
いかかでしたでしょうか?RealmとCodableを使うことで、データのやり取りも含めたデータベースが取り組みやすくなるかと思います。
続編書きました!
【Swift4】Realm+Codableを使ったお手軽なDB Part.2(リレーション編)
【Swift4】Realm+Codableを使ったお手軽なDB Part.3(クエリ編)
【Swift4】Realm+Codableを使ったお手軽なDB Part.4(番外編)
株式会社Nexceed にて、一緒に働いてくれる仲間を募集中です