[Swift][Realm] Realm に Swift の enum を保存する方法

     

TAGS:

Realm 10.10.0 より以前のバージョンでは、enum は Int と RawRepresentable に準拠していないと保存できませんでしたが、Realm 10.10.0 以降は より広いケースに対応しています。Realm 10.10 で可能になった方法をまとめます。
以下の環境で動作確認を行なっています。

  • macOS Monterey 12.1 RC
  • Xcode 13.2 RC
  • iOS 15.2
  • Realm 10.21.1

Swift の enum

Swift での enum は、associatedValue を持てるなど、C 言語の enum よりもずいぶん拡張されています。

enum を上手につかうと、状態を制限することで不要なエラーを減らしたり、コードの意図を読みやすくしたりすることができます。

モデルに使うことで”読みやすくなる”と思っていたのですが、Realm で保存しようとするとすこし手間取るので、説明していきます。

enum での associated Value の使い方は、以下の記事で説明しています。
Swift[Swift] enum の associated Value へのアクセス

enum を持つモデル

Realm に保存するためのモデルに、 enum を使ったケースを考えていきます。

simple な enum

まずは、simple な enum もつ class を考えます。

以下のコードの TagType は、Priority もしくは DueDate という状態を持つように宣言しています。

Simple な Model 例

class TODOTag: Object, Identifiable {
    @Persisted var id: UUID = UUID()
    @Persisted var type: TagType

   public enum TagType: String, RawRepresentable {
        case Priority, DueDate
    }
}

この TODOTag をRealm に保存することを考えていきます。

Realm 10.10.0 より前のバージョンでは、enum は、Int と RawRepresentable に準拠しなければいけなかったので、上記は、そのままでは保存できないモデルでした。

associatedValue を持つ enum

次に、associatedValue を持つ enum を使った Model を考えてみます。

assoiatedValue を持つ enum を使った例

class TODOTag: Object, Identifiable {
    @Persisted var id: UUID = UUID()
    @Persisted var type: TagType

   public enum TagType {
        case Priority(value: Priority)
        case DueDate( due: Date)
    }
}
public enum Priority: String, RawRepresentable {
      case High, Mid, Low
      public init?(rawValue: String) {
          switch rawValue {
          case Priority.High.rawValue:
              self = .High
          case Priority.Mid.rawValue:
              self = .Mid
          case Priority.Low.rawValue:
              self = .Low
          default:
              return nil
          }
      }
  }

Tag のタイプだけではなく、そのタイプに関連したデータを保持させるような enum を定義しました。具体的には、Priority のタグには、associatedValue として優先度(enum Priority)を付与し、DueDate(締切)のタグには、associatedValue を利用して、締切日(Date)を付与しています。

enum の Realm への保存

simple な enum の保存

Realm10.10.0 以降では、(Int だけではなく)RawRepresentable な enum を保存できるようになりました。

以下のように、enum を PersistableEnum に準拠させ、enum を保持するプロパティに、@Persisted と指定することで保存できるようになります。

Simple な Model 例

class TODOTag: Object, Identifiable {
    @Persisted var id: UUID = UUID()
    @Persisted var type: TagType

   public enum TagType: String, RawRepresentable, PersistableEnum {
        case Priority, DueDate
    }
}

上記で保存されるようになります。特別な変換等は不要です。

なお、Realm 10.10.0 より前のバージョンでは、Int で、RawRepresentable な enum しか保存できませんでした。

associatedValue を持つ enum の保存

Realm で associatedValue を持つ enum を保存しようとする時に、いくつかの制約があります。

1つ目が、”Realm では、associatedValue を持つ enum をそのまま保存することはできない”という点です。

つまり、アプリ側で分解して、associatedValue → enum + 値 と分解して、それぞれを保存する必要があります。

"enum" 部分は、上に書いたように PersistableEnum に準拠させることで保存できますが、"値" の部分が問題です。

Swift の enum の associatedValue は、case 毎に型とその数について異なる値を持つことができます。

型の違いは、AnyRealmValue という型を使うことで対応します。

なお、AnyRealmValue が持つことができるのは以下の型 "だけ" です。

  • Int
  • Float
  • Double
  • Decimal128
  • ObjectID
  • UUID
  • Bool
  • Date
  • Data
  • String
  • Object

異なる型で表されている場合は何らかの変換も必要となります。今回のケースでは、case Priority の associatedValue は、enum Priority で保持していますが、そのままでは AnyRealmValue として保存することはできません。

以下では、例に示した(そのままでは変換できない) associatedValue つき enum を保存することを考えます。

enum は、AnyRealmValue には保存できないため、String として保存することにします。

なお、associatedValue として複数の値を持つ時には、(Realmの)Map 等を使っての対応が必要となります。(本記事では説明しません)

assocaitedValue 付き enum を分解した定義

class TODOTag: Object, Identifiable {
    @Persisted var id: UUID = UUID()
    @Persisted var type: TagType
    @Persisted var typeValue: AnyRealmValue

   // 本当は以下を保存したい(がそのままではできない)
   //@Persisted var info: TagInfoType
   //enum TagInfoType {
   //     case Priority(value: Priority)
   //      case DueDate(date: Date)
   //}
}

associatedValue を保持する型として AnyRealmObject を使用しますが、さまざまな型をそのまま代入できるわけではありません。AnyRealmObject は値をセットする時に保存にしようする型を指定するようにしてセットする必要があります。

以下は、String 型のオブジェクトを保存するときの書き方です。

AnyRealmObject へのセット例

self.anyObject = .string("Hello world")

このように associatedValue 付き enum → simple な enum + AnyRealmObject と分解することで、associatedValue 付き enum 相当の情報を保存できるようになります。

そのままでも運用できると思いますが、外部向けに associatedValue 付き enum として取得したいのであれば、別途 変換が必要となります。

以下は、外部向けに associatedValue 付きの enum である TODOTagInfo を取得するコードを追加しています。TODOTagInfo は、Realm に保存している TagInfo から情報を取得して生成しています。

なお、Realm は、取得したオブジェクトに変更が "Live" に反映されるので、TODOTag 自体を取得した時点で、その内部まで変換してしまうのはお勧めできません。(ケースバイケースですが・・・)
パフォーマンス上の問題が内容であれば、この例のように、情報が要求されたタイミングで変換するのが良さそうです。

associatedValue 付き enum への変換

public enum TODOTagInfo {
    case Priority(value: Priority)
    case DueDate(due: Date)
}

class TODOTag: Object, Identifiable {
    @Persisted var id: UUID = UUID()
    @Persisted var type: TagType
    @Persisted var typeValue: AnyRealmValue
    
    public enum TagType: String, RawRepresentable, PersistableEnum {
        case Priority, DueDate
    }
    
    var tagInfo: TODOTagInfo? {
        switch self.type {
        case .Priority:
            if let prio = self.priority {
                return TODOTagInfo.Priority(value: prio)
            }
        case .DueDate:
            if let due = self.dueDate {
                return TODOTagInfo.DueDate(due: due)
            }
        }
        return nil
    }

    var priority: Priority? {
        get {
            guard type == .Priority else { return nil }
            if let strValue = self.typeValue.stringValue {
                return Priority.init(rawValue: strValue)
            }
            return nil
        }
        set(newValue) {
            type = .Priority
            self.typeValue = .string(newValue?.rawValue ?? Priority.Mid.rawValue)
        }
    }
    
    var dueDate: Date? {
        get {
            typeValue.dateValue
        }
        set(newValue) {
            self.type = .DueDate
            if let newDate = newValue {
                self.typeValue = .date(newDate)
            }
        }
    }
}

まとめ

enum を Realm に保存する方法を見てきました。

enum を Realm に保存する方法
  • RawRepresentable な enum は、PersistableEnum に準拠させると保存できる
  • associatedValue 付きの enum は、分解して保存する必要がある

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。