【Swift4】Realm+Codableを使ったお手軽なDB Part.1（モデル編）

# はじめに
みなさん、こんにちは。株式会社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](https://realm.io/jp)
インストール方法はこの記事では割愛しますが、CocoaPodsでインストール出来ます。

### Codable
Swift4から新たにFoundationに追加され、簡単にJSONのデコード、エンコードを行うことができる仕組みが提供されました。これを使うことで、他のフレームワークなしに、複雑なJSONも一発で変換できます！


# まずやってみる


今回は以下のようなJSONデータを利用するとします。

```json
{
    "name": "hogehoge",
    "id": 553,
}
```

## モデルの作成

このデータを格納するための、モデルクラスを作成します。Codableに準拠させるので、JSONパースができ、そのままRealmのテーブルを作るためのクラスとしても使用できます。

```UserData.swift
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の作業をやってみましょう。

## 実行

```swift
// 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を使用することで確認できます。
<img width="1089" alt="スクリーンショット 2018-10-27 18.04.08.png" src="https://qiita-image-store.s3.amazonaws.com/0/97981/fe0b165c-a661-3a39-114a-c6f2bfee822f.png">
ちゃんと追加できることが確認できました！
今度はRealmからデータを取得し、JSON形式として出力してみます！

```swift
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形式の文字列が出力されました😃
![スクリーンショット 2018-10-27 18.12.39.png](https://qiita-image-store.s3.amazonaws.com/0/97981/948fa6fd-cd25-fd9c-59fe-63652bc5ebea.png)


## ポイント
### 変数

変数は`@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](https://realm.io/docs/swift/latest/) -> Property cheatsheetより引用）

### 自動Codable準拠 
String, Int, Double, Date, Data, URLなどは既にCodableに準拠しているため、明示的にencode,decodeメソッドを実装する必要はありません。しかし、Date型やオプショナル型などそのままでは使えない場合があるので、自分でencode,decodeメソッドを実装する必要があります。


# 応用編
様々な型のデータ型に対応できるように、encode,decodeメソッドを自前で実装する場合をまとめます。

```json
{
    "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クラスを編集します。

```UserData.swift
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（リレーション編）](https://qiita.com/cottpan/items/4bb5ad8bce2e28c0220a)
[【Swift4】Realm+Codableを使ったお手軽なDB Part.3（クエリ編）](https://qiita.com/cottpan/items/fcdadc869d1d57a9aa18)
[【Swift4】Realm+Codableを使ったお手軽なDB Part.4（番外編）](https://qiita.com/cottpan/items/e9647991936f249dfb32)

***
[株式会社Nexceed](https://www.wantedly.com/companies/nexceed) にて、一緒に働いてくれる仲間を募集中です:point_down::point_down::point_down:
 - [建築業界に革命を！！AI × BIMの世界を作り出すアプリエンジニア募集！](https://www.wantedly.com/companies/nexceed)