post Image
【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
インストール方法はこの記事では割愛しますが、CocoaPodsでインストール出来ます。

Codable

Swift4から新たにFoundationに追加され、簡単に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の作業をやってみましょう。

実行

// 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を使用することで確認できます。
スクリーンショット 2018-10-27 18.04.08.png
ちゃんと追加できることが確認できました!
今度は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形式の文字列が出力されました😃
スクリーンショット 2018-10-27 18.12.39.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 -> 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クラスを編集します。

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(リレーション編)
【Swift4】Realm+Codableを使ったお手軽なDB Part.3(クエリ編)
【Swift4】Realm+Codableを使ったお手軽なDB Part.4(番外編)


株式会社Nexceed にて、一緒に働いてくれる仲間を募集中です:point_down::point_down::point_down:
建築業界に革命を!!AI × BIMの世界を作り出すアプリエンジニア募集!


『 Swift 』Article List