GyroでxcdatamodelからRealmのモデルクラス定義をコード生成


GyroというxcdatamodelからKotlin/Java/Swiftのモデル定義コードを生成する神ツールが爆誕していたので使ってみた。
https://github.com/NijiDigital/gyro

xcdatamodelはMac/iOS開発者以外には馴染みがないと思うが、Appleが開発したCore Dataというデータ管理のための開発フレームワークがあり、そのデータモデル定義の成果物がxcdatamodelになると思ってもらえば良い。

手っ取り早くQiitaのArticle、Tag、Userの関係を例にとってxcdatamodelでデータモデルを作って、gyroで吐き出されたモデルクラスをコピペするので、眺めてみていただきたい。

データ定義

Relation

Article

User

Tag

コード生成

上記のデータ定義(Qiita.xcdatamodeld/Model.xcdatamodel)からGyroを使ってSwiftの定義ファイルを生成する。

gyro --model Qiita.xcdatamodeld/Model.xcdatamodel --template swift3 --output Generated

生成されたコードは以下の通り。

Article.swift
/* DO NOT EDIT | Generated by gyro */

import RealmSwift
import Foundation

final class Article: Object {

  enum Attributes: String {
    case id = "id" /* Primary Key */
    case body = "body"
    case commentsCount = "commentsCount"
    case createdAt = "createdAt"
    case likesCount = "likesCount"
    case title = "title"
    case updatedAt = "updatedAt"
    case url = "url"
  }

  enum Relationships: String {
    case tags = "tags"
    case user = "user"
  }

  @objc dynamic var id: String? /* Primary Key */
  @objc dynamic var body: String?
  let commentsCount = RealmOptional<Int64>()
  @objc dynamic var createdAt: Date?
  let likesCount = RealmOptional<Int64>()
  @objc dynamic var title: String?
  @objc dynamic var updatedAt: Date?
  @objc dynamic var url: String?
  let tags = List<Tag>()
  @objc dynamic var user: User?

  override static func primaryKey() -> String? {
    return "id"
  }

}
User.swift
/* DO NOT EDIT | Generated by gyro */

import RealmSwift
import Foundation

final class User: Object {

  enum Attributes: String {
    case id = "id" /* Primary Key */
    case name = "name"
    case profileImageUrl = "profileImageUrl"
  }

  @objc dynamic var id: String? /* Primary Key */
  @objc dynamic var name: String?
  @objc dynamic var profileImageUrl: String?

  let articles = LinkingObjects(fromType: Article.self, property: "user")

  override static func primaryKey() -> String? {
    return "id"
  }

}
Tag.swift
/* DO NOT EDIT | Generated by gyro */

import RealmSwift
import Foundation

final class Tag: Object {

  enum Attributes: String {
    case id = "id" /* Primary Key */
    case iconUrl = "iconUrl"
  }

  @objc dynamic var id: String? /* Primary Key */
  @objc dynamic var iconUrl: String?

  let articles = LinkingObjects(fromType: Article.self, property: "tags")

  override static func primaryKey() -> String? {
    return "id"
  }

}

使ってみた感想

  • データ定義を考えるときとそれをコードでゴリゴリ書いていくときとでメンタルモデルの切り替えが必要なくなる。
  • 単一のスキーマからAndroid/iOSそれぞれのモデルクラスを生成できるので生産性が良い。
  • Xcode半強制なので紛争の可能性。実体はXMLなのでXcode必須でないが、謎XMLでスキーマ定義するぐらいならコードを書いた方がマシとなりそう。
  • xcdatamodelはレビューしてて気持ちよくない。xcdatamodelにはGUI上での配置情報といったレビュー時にはどうでも良い情報も含まれているためノイズが混じりやすく、肝心なスキーマの変更差分がわかりづらいかもしれない。

GUI特有の難点があるのでチーム開発に導入するには意見が分かれるかも。個人で使う分には非常に便利で楽しいツールなのでどんどん使っていく。

ちなみにxcdatamodelはこんな感じのXML。

Qiita.xcdatamodeld/Model.xcdatamodel/contents
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="13772" systemVersion="17D47" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
    <entity name="Article" representedClassName="Article" syncable="YES" codeGenerationType="class">
        <attribute name="body" optional="YES" attributeType="String" syncable="YES"/>
        <attribute name="commentsCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
        <attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
        <attribute name="id" optional="YES" attributeType="String" syncable="YES"/>
        <attribute name="likesCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
        <attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
        <attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
        <attribute name="url" optional="YES" attributeType="String" syncable="YES"/>
        <relationship name="tags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Tag" inverseName="article_" inverseEntity="Tag" syncable="YES"/>
        <relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="User" inverseName="article_" inverseEntity="User" syncable="YES"/>
        <userInfo>
            <entry key="identityAttribute" value="id"/>
        </userInfo>
    </entity>
    <entity name="Tag" representedClassName="Tag" syncable="YES" codeGenerationType="class">
        <attribute name="iconUrl" optional="YES" attributeType="String" syncable="YES"/>
        <attribute name="id" optional="YES" attributeType="String" syncable="YES"/>
        <relationship name="article_" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Article" inverseName="tags" inverseEntity="Article" syncable="YES"/>
        <userInfo>
            <entry key="identityAttribute" value="id"/>
        </userInfo>
    </entity>
    <entity name="User" representedClassName="User" syncable="YES" codeGenerationType="class">
        <attribute name="id" optional="YES" attributeType="String" syncable="YES"/>
        <attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
        <attribute name="profileImageUrl" optional="YES" attributeType="String" syncable="YES"/>
        <relationship name="article_" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Article" inverseName="user" inverseEntity="Article" syncable="YES"/>
        <userInfo>
            <entry key="identityAttribute" value="id"/>
        </userInfo>
    </entity>
    <elements>
        <element name="Article" positionX="-324" positionY="-91" width="128" height="193"/>
        <element name="User" positionX="-101" positionY="72" width="128" height="103"/>
        <element name="Tag" positionX="-101" positionY="-54" width="128" height="90"/>
    </elements>
</model>