Swift Codable で Cloud Firestore のデータをマッピングする

Swift 4 で導入された Swift Codable API を使用すると、コンパイラの機能を利用して、シリアル化された形式のデータを Swift 型に簡単にマッピングすることができます。

Codable を使用してウェブ API からアプリのデータモデルにデータをマッピングしたり、その逆のマッピングをしたことがあるかもしれませんが、それよりもはるかに柔軟な処理を行うことができます。

このガイドでは、Codable を使用して Cloud Firestore と Swift データ型の間でデータをマッピングする方法について説明します。

Cloud Firestore からドキュメントを取得するときに、アプリは Key-Value ペアの辞書(複数のドキュメントを返すオペレーションの場合は辞書の配列)を受け取ります。

これにより、Swift で引き続き辞書を使用できるため、ユースケースで実際に求められる柔軟性を実現できます。しかし、このアプローチでは型安全性が保証されません。属性名のスペルを間違えたり、新しい機能をリリースするときに、追加した属性のマッピングを忘れてしまうなど、追跡困難なバグが発生しやすくなります。

こうした欠点に対処するため、多くのデベロッパーは辞書を Swift 型にマッピングするシンプルなマッピング レイヤを実装してきました。ただし、こうした実装でも、Cloud Firestore ドキュメントとそれに対応するアプリのデータモデルとの間のマッピングの指定は手動で行われています。

Cloud Firestore で Swift の Codable API がサポートされるようになったため、多くのことが簡単になりました。たとえば

  • マッピング コードを手動で実装する必要がなくなりました。
  • 属性を異なる名前にマッピングする方法を簡単に定義できます。
  • 多くの Swift 型に対するサポートが組み込まれています。
  • カスタム型のマッピングも簡単に追加できます。
  • さらに、シンプルなデータモデルの場合、マッピング コードを記述する必要はありません。

マッピング データ

Cloud Firestore は、キーと値をマッピングするデータをドキュメントに保存します。個々のドキュメントからデータを取得するには、DocumentSnapshot.data() を呼び出します。これは、フィールド名を Any:func data() -> [String : Any]? にマッピングする辞書を返します。

つまり、Swift の subscript 構文を使用して個々のフィールドにアクセスできます。

import FirebaseFirestore

#warning("DO NOT MAP YOUR DOCUMENTS MANUALLY. USE CODABLE INSTEAD.")
func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)

  docRef.getDocument { document, error in
    if let error = error as NSError? {
      self.errorMessage = "Error getting document: \(error.localizedDescription)"
    }
    else {
      if let document = document {
        let id = document.documentID
        let data = document.data()
        let title = data?["title"] as? String ?? ""
        let numberOfPages = data?["numberOfPages"] as? Int ?? 0
        let author = data?["author"] as? String ?? ""
        self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author)
      }
    }
  }
}

このコードはシンプルで実装も簡単にできそうです。しかし、これは脆弱なコードです。保守は難しく、エラーも発生しやすくなります。

ご覧のとおり、ここではドキュメント フィールドのデータ型を前提としていますが、これが正しい場合もあれば、そうでない場合もあります。

スキーマがないため、新しいドキュメントをコレクションに簡単に追加し、フィールドに別の型を選択することもできます。たとえば、numberOfPages フィールドに誤って文字列型を選択すると、マッピングの問題を見つけるのが難しくなります。新しいフィールドが追加されるたびに、マッピング コードの更新が必要になりますが、この作業は煩雑です。

また、Book の各プロパティについて適切な型を正確に認識できる Swift の強力な型システムも利用できません。

Codable とは

Apple のドキュメントによると、Codable は「外部表現との間で相互に変換可能な型」とされています。実際、Codable は Encodable プロトコルと Decodable プロトコルの型の別名です。Swift 型をこのプロトコルに準拠すると、コンパイラはこの型のインスタンスを JSON などのシリアル化された形式からエンコード / デコードするために必要なコードを生成します。

たとえば、書籍に関するデータを保存するシンプルな型は次のようになります。

struct Book: Codable {
  var title: String
  var numberOfPages: Int
  var author: String
}

このように型を Codable にすることで影響は最小限になります。プロトコルに準拠させるだけで、その他の変更は必要ありません。

これにより、書籍に関するデータを JSON オブジェクトに簡単にエンコードできるようになりました。

do {
  let book = Book(title: "The Hitchhiker's Guide to the Galaxy",
                  numberOfPages: 816,
                  author: "Douglas Adams")
  let encoder = JSONEncoder()
  let data = try encoder.encode(book)
} 
catch {
  print("Error when trying to encode book: \(error)")
}

JSON オブジェクトを Book インスタンスにデコードするには、次のように宣言します。

let decoder = JSONDecoder()
let data = /* fetch data from the network */
let decodedBook = try decoder.decode(Book.self, from: data)

Codable を使用して Cloud Firestore ドキュメントの単純な型をマッピングする

Cloud Firestore は、単純な文字列からネストされたマップまで、幅広いデータ型をサポートしています。これらのほとんどは Swift の組み込み型に直接対応しています。複雑なデータ型を扱う前に、いくつかの単純なデータ型を見てみましょう。

Cloud Firestore ドキュメントを Swift 型にマッピングするには、次の操作を行います。

  1. FirebaseFirestore フレームワークがプロジェクトに追加されていることを確認します。Swift パッケージ マネージャーか CocoaPods を使用します。
  2. FirebaseFirestore を Swift ファイルにインポートします。
  3. 型を Codable に準拠させます。
  4. List ビューで型を使用する場合は省略可)id プロパティを型に追加します。@DocumentID を使用して、これをドキュメント ID にマッピングすることを Cloud Firestore に通知します。詳細については後ほど説明します。
  5. documentReference.data(as: ) を使用して、ドキュメント参照を Swift 型にマッピングします。
  6. documentReference.setData(from: ) を使用して、Swift 型のデータを Cloud Firestore ドキュメントにマッピングします。
  7. (省略可、ただし強く推奨)適切なエラー処理を実装します。

では、Book 型を更新してみましょう。

struct Book: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
}

この型はすでに Codable になっているため、id プロパティを追加して @DocumentID プロパティ ラッパーでアノテーションを付けるだけで済みます。

ドキュメントの取得とマッピングに前述のコード スニペットを使用すると、すべての手動マッピング コードを 1 行で置き換えることができます。

func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)

  docRef.getDocument { document, error in
    if let error = error as NSError? {
      self.errorMessage = "Error getting document: \(error.localizedDescription)"
    }
    else {
      if let document = document {
        do {
          self.book = try document.data(as: Book.self)
        }
        catch {
          print(error)
        }
      }
    }
  }
}

getDocument(as:) を呼び出すときにドキュメントの型を指定すると、より簡潔に記述できます。これにより、マッピングが実行され、マッピングされたドキュメントを含む Result 型が返されます。デコードに失敗した場合はエラーが返されます。

private func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)
  
  docRef.getDocument(as: Book.self) { result in
    switch result {
    case .success(let book):
      // A Book value was successfully initialized from the DocumentSnapshot.
      self.book = book
      self.errorMessage = nil
    case .failure(let error):
      // A Book value could not be initialized from the DocumentSnapshot.
      self.errorMessage = "Error decoding document: \(error.localizedDescription)"
    }
  }
}

既存のドキュメントを更新する場合は、documentReference.setData(from: ) を呼び出すだけです。Book インスタンスを保存するコードは次のとおりです(基本的なエラー処理も含まれています)。

func updateBook(book: Book) {
  if let id = book.id {
    let docRef = db.collection("books").document(id)
    do {
      try docRef.setData(from: book)
    }
    catch {
      print(error)
    }
  }
}

新しいドキュメントを追加すると、そのドキュメントに新しいドキュメント ID が自動的に割り当てられます。これは、アプリがオフラインの場合でも機能します。

func addBook(book: Book) {
  let collectionRef = db.collection("books")
  do {
    let newDocReference = try collectionRef.addDocument(from: self.book)
    print("Book stored with new document reference: \(newDocReference)")
  }
  catch {
    print(error)
  }
}

Cloud Firestore は、単純なデータ型のマッピングだけでなく、多くのデータ型をサポートしています。その型の一部は構造化されているため、ドキュメント内にネストされたオブジェクトを作成できます。

ネストされたカスタム型

ドキュメントでマッピングする属性のほとんどは、書籍のタイトルや著者名のような単純な値です。では、より複雑なオブジェクトを保存する必要がある場合、たとえば、解像度に合わせて書籍の表紙への URL を変えたい場合はどうすればよいでしょうか。

Cloud Firestore でこれを実現する最も簡単な方法はマップの使用です。

Firestore ドキュメントにネストされたカスタム型を保存する

対応する Swift 構造体を記述するときに、Cloud Firestore で URL がサポートされている点を利用します。URL を含むフィールドを格納すると、このフィールドは文字列に変換されます(その逆も同様です)。

struct CoverImages: Codable {
  var small: URL
  var medium: URL
  var large: URL
}

struct BookWithCoverImages: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var cover: CoverImages?
}

Cloud Firestore ドキュメントで表紙のマップに構造体 CoverImages を定義しています。BookWithCoverImages の cover プロパティをオプションとして設定しているので、cover 属性が含まれていないドキュメントにも対応できます。

データの取得や更新を行うコード スニペットがないことに疑問を感じるかもしれませんが、Cloud Firestore に対して読み書きを行うためにコードを調整する必要はありません。最初のセクションで記述したコードで十分対応できます。

配列

値のコレクションをドキュメントに保存したい場合もあります。たとえば、書籍のジャンルなどは良い例でしょう。『The Hitchhiker's Guide to the Galaxy』のような本は、SF、コメディなど、複数のカテゴリに分類できます。

Firestore ドキュメントに配列を保存する

Cloud Firestore では、値の配列を使用してこれをモデル化できます。この処理はすべての Codable 型(StringInt など)でサポートされています。以下に、Book モデルにジャンルの配列を追加する方法を示します。

public struct BookWithGenre: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var genres: [String]
}

これは、すべての Codable 型で機能します。カスタム型も同様です。各書籍のタグのリストを保存してみましょう。タグの名前に加えて、次のようにタグの色も保存します。

Firestore ドキュメントにカスタム型の配列を保存する

この方法でタグを保存するには、タグを表す Tag 構造体を実装して Codable にします。

struct Tag: Codable, Hashable {
  var title: String
  var color: String
}

このように、Tags の配列を Book ドキュメントに格納できます。

struct BookWithTags: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var tags: [Tag]
}

ドキュメント ID のマッピングについて

ほかの型のマッピングに進む前に、ドキュメント ID のマッピングについて少し説明しましょう。

これまでの例の一部では、@DocumentID プロパティ ラッパーを使用して、Cloud Firestore ドキュメントのドキュメント ID を Swift 型の id プロパティにマッピングしました。これには多くの理由があります。

  • ユーザーがローカルで変更を加えた場合に更新が必要なドキュメントを簡単に把握できます。
  • SwiftUI の List では、挿入時に要素のジャンプを防ぐため、要素を Identifiable にする必要があります。

@DocumentID とマークされた属性は、ドキュメントを書き戻す際に Cloud Firestore のエンコーダによってエンコードされません。これは、ドキュメント ID がドキュメント自体の属性ではないためです。このような ID をドキュメントに書き込むのは誤りです。

ネストされた型(前の例の Book のタグの配列など)を扱う場合、@DocumentID プロパティを追加する必要はありません。ネストされたプロパティは Cloud Firestore ドキュメントの一部であり、別のドキュメントを構成するものではありません。したがって、ドキュメント ID は必要ありません。

日付と時刻

Cloud Firestore には日付と時刻を処理するためのデータ型が組み込まれています。Cloud Firestore で Codable がサポートされているため、これらのデータ型を簡単に使用できます。

次のドキュメントをみてください。これは、1843 年に世界で初めてプログラミング言語を考案した Ada を表しています。

Firestore ドキュメントに日付を保存する

このドキュメントをマッピングする Swift 型は次のようになります。

struct ProgrammingLanguage: Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
}

日付と時刻を扱うこのセクションでは、@ServerTimestamp についても説明しておく必要があります。アプリのタイムスタンプを扱うときに、このプロパティ ラッパーは強化なツールとなります。

どの分散システムでも、個々のシステムのクロックが常に完全に同期しているとは限りません。これは大した問題ではないと思うかもしれませんが、株式取引の世界ではわずかな時間のずれが大きな影響を及ぼすことになります。たとえミリ秒であっても、数百万ドルの損失を被る可能性があります。

Cloud Firestore は、@ServerTimestamp でマークされた属性を次のように処理します。属性を保存するときに(たとえば addDocument() を使用して保存する)、属性が nil であれば、Cloud Firestore はフィールドにデータベースに書き込む時点のサーバー タイムスタンプを挿入します。addDocument() または updateData() を呼び出したときに、このフィールドが nil でない場合、Cloud Firestore は属性値を変更しません。このように、createdAtlastUpdatedAt などのフィールドを簡単に実装できます。

地理的位置

Google のアプリでは、いたるところで位置情報が使用されています。この情報を保存することで、多くの魅力的な機能が実現されています。たとえば、タスクに関連する位置情報を保存しておくことで、その場所に到達したときに、アプリでタスクの通知を表示することができます。

Cloud Firestore に組み込まれているデータ型 GeoPoint を使用すると、任意の場所の緯度と経度を保存できます。Cloud Firestore ドキュメントとの間でやり取りする位置情報をマッピングするには、GeoPoint 型を使用します。

struct Office: Codable {
  @DocumentID var id: String?
  var name: String
  var location: GeoPoint
}

Swift でこれに対応する型は CLLocationCoordinate2D で、この 2 つの型のマッピングは次のように行います。

CLLocationCoordinate2D(latitude: office.location.latitude,
                      longitude: office.location.longitude)

所在地でドキュメントをクエリする詳しい方法については、こちらのソリューション ガイドをご覧ください。

列挙型

列挙型は、Swift で最も過小評価されている言語機能の一つではないでしょうか。しかし、実際には見た目以上の価値があります。列挙型は離散状態をモデル化する際によく使用されます。たとえば、記事を管理するアプリを作成するとします。記事のステータスを追跡するには、列挙型の Status を使用します。

enum Status: String, Codable {
  case draft
  case inReview
  case approved
  case published
}

Cloud Firestore では、列挙型は直接サポートされていません(つまり、一連の値を適用することはできません)が、列挙型の型指定で Codable 型を選択することは可能です。この例では String を選択していますが、これは、Cloud Firestore ドキュメントに保存されるときに、すべての列挙値が文字列型にマッピングされること(あるいはその逆)を意味します。

Swift では、未加工のカスタム値がサポートされているため、どの値がどの列挙型のケースを参照するかをカスタマイズできます。たとえば、Status.inReview ケースを「in review」として保存する場合は、前述の列挙型を次のように更新します。

enum Status: String, Codable {
  case draft
  case inReview = "in review"
  case approved
  case published
}

マッピングのカスタマイズ

マッピングする Cloud Firestore ドキュメントの属性名が、Swift のデータモデルのプロパティ名と一致しないことがあります。たとえば、同僚の 1 人が Python で開発を行い、すべての属性名に snake_case を選択したとします。

その場合も Codable で対応できます。

このような場合は CodingKeys を使用します。これは、特定の属性のマッピング方法を指定する Codable 型の構造体に追加できる列挙型です。

次のドキュメントについて考えてみましょう。

snake_cased 属性名を含む Firestore ドキュメント

このドキュメントを String 型の name プロパティを含む構造体にマッピングするには、CodingKeys 列挙型を ProgrammingLanguage 構造体に追加し、ドキュメントの属性名を指定する必要があります。

struct ProgrammingLanguage: Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
  
  enum CodingKeys: String, CodingKey {
    case id
    case name = "language_name"
    case year
  }
}

デフォルトでは、Codable API は Swift 型のプロパティ名を使用して、マッピングする Cloud Firestore ドキュメントの属性名を判別します。そのため、属性名が一致していれば、Codable 型に CodingKeys を追加する必要はありません。ただし、特定の型に CodingKeys を使用する場合は、マッピングするプロパティ名をすべて追加する必要があります。

前述のコード スニペットでは id プロパティを定義しましたが、これを SwiftUI の List ビューで識別子として使用できます。CodingKeys で指定しない場合、データの取得時にマッピングされないため、nil になります。これにより、最初のドキュメントに List ビューが埋め込まれます。

それぞれの CodingKeys 列挙型のケースとしてリストされていないプロパティは、マッピング プロセス中に無視されます。これは、一部のプロパティをマッピングの対象外にする場合に特に便利です。

たとえば、reasonWhyILoveThis プロパティをマッピングの対象外にする場合は、それを CodingKeys 列挙型から削除するだけですみます。

struct ProgrammingLanguage: Identifiable, Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
  var reasonWhyILoveThis: String = ""
  
  enum CodingKeys: String, CodingKey {
    case id
    case name = "language_name"
    case year
  }
}

また、空の属性を Cloud Firestore ドキュメントに書き戻すこともあります。Swift には、値が存在しないことを示すオプションがありますが、Cloud Firestore でも null 値がサポートされています。ただし、デフォルトでは nil 値を含むオプションのエンコードは省略されます。@ExplicitNull を使用すると、エンコードでの Swift オプションの処理方法を制御できます。オプションのプロパティを @ExplicitNull として設定すると、nil という値が含まれている場合に、null 値を使用してこのプロパティをドキュメントに書き込むよう Cloud Firestore に指示できます。

カラー マッピングでのカスタム エンコーダ / デコーダの使用

Codable を使用してデータをマッピングする方法の最後のトピックとして、カスタム エンコーダ / デコーダについて説明します。このセクションではネイティブの Cloud Firestore データ型については触れませんが、カスタム エンコーダ / デコーダは Cloud Firestore アプリでよく利用されています。

デベロッパーの方から「色はどのようにマッピングするのか」という質問をよく聞かれます。これは Cloud Firestore だけでなく、Swift と JSON 間のマッピングでもよくある質問です。数多くのソリューションがありますが、その大半は JSON に焦点を当てており、ほとんどが RGB コンポーネントで構成されるネストされた辞書として色をマッピングしています。

しかし、よりシンプルで適切なソリューションがあるはずです。ウェブの色を使用するのも一つの方法です(具体的には、CSS の 16 進数の色コードを使用します)。これらは使いやすく、本質的には文字列であり、透明度もサポートされています。

Swift の Color を 16 進数値にマッピングするには、Codable を Color に追加する Swift 拡張機能を作成する必要があります。

extension Color {

 init(hex: String) {
    let rgba = hex.toRGBA()

    self.init(.sRGB,
              red: Double(rgba.r),
              green: Double(rgba.g),
              blue: Double(rgba.b),
              opacity: Double(rgba.alpha))
    }

    //... (code for translating between hex and RGBA omitted for brevity)

}

extension Color: Codable {
  
  public init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let hex = try container.decode(String.self)

    self.init(hex: hex)
  }
  
  public func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    try container.encode(toHex)
  }

}

decoder.singleValueContainer() を使用すると、RGBA コンポーネントをネストすることなく、String を同等の Color にデコードできます。また、これらの値を変換しなくても、アプリのウェブ UI で使用できます。

これにより、タグのマッピングのコードを更新できます。アプリの UI コードで手動でマッピングする必要がなくなり、タグの色を直接処理しやすくなります。

struct Tag: Codable, Hashable {
  var title: String
  var color: Color
}

struct BookWithTags: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var tags: [Tag]
}

エラー処理

上のコード スニペットでは、意図的に最小限のエラー処理にしていますが、本番環境のアプリでは、すべてのエラーを適切に処理する必要があります。

次のコード スニペットは、発生する可能性のあるすべてのエラー状況に対応する方法を示しています。

class MappingSimpleTypesViewModel: ObservableObject {
  @Published var book: Book = .empty
  @Published var errorMessage: String?
  
  private var db = Firestore.firestore()
  
  func fetchAndMap() {
    fetchBook(documentId: "hitchhiker")
  }
  
  func fetchAndMapNonExisting() {
    fetchBook(documentId: "does-not-exist")
  }
  
  func fetchAndTryMappingInvalidData() {
    fetchBook(documentId: "invalid-data")
  }
  
  private func fetchBook(documentId: String) {
    let docRef = db.collection("books").document(documentId)
    
    docRef.getDocument(as: Book.self) { result in
      switch result {
      case .success(let book):
        // A Book value was successfully initialized from the DocumentSnapshot.
        self.book = book
        self.errorMessage = nil
      case .failure(let error):
        // A Book value could not be initialized from the DocumentSnapshot.
        switch error {
        case DecodingError.typeMismatch(_, let context):
          self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
        case DecodingError.valueNotFound(_, let context):
          self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
        case DecodingError.keyNotFound(_, let context):
          self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
        case DecodingError.dataCorrupted(let key):
          self.errorMessage = "\(error.localizedDescription): \(key)"
        default:
          self.errorMessage = "Error decoding document: \(error.localizedDescription)"
        }
      }
    }
  }
}

ライブ更新エラーの処理

前述のコード スニペットは、単一ドキュメントを取得する際のエラーを処理する方法を示しています。Cloud Firestore では、データを 1 回取得するだけでなく、いわゆるスナップショット リスナーを使用して、アプリが更新されるたびにアップデートを配信することもできます。コレクション(またはクエリ)にスナップショット リスナーを登録すると、更新が行われるたびに、Cloud Firestore はリスナーを呼び出します。

次のコード スニペットは、スナップショット リスナーの登録方法、Codable を使用してデータをマッピングする方法、発生する可能性のあるすべてのエラーの対処方法を示したものです。また、新しいドキュメントをコレクションに追加する方法も示しています。マッピングされたドキュメント自体を保持するローカル配列を更新する必要はありません。これは、スナップショット リスナーのコードによって処理されます。

class MappingColorsViewModel: ObservableObject {
  @Published var colorEntries = [ColorEntry]()
  @Published var newColor = ColorEntry.empty
  @Published var errorMessage: String?
  
  private var db = Firestore.firestore()
  private var listenerRegistration: ListenerRegistration?
  
  public func unsubscribe() {
    if listenerRegistration != nil {
      listenerRegistration?.remove()
      listenerRegistration = nil
    }
  }
  
  func subscribe() {
    if listenerRegistration == nil {
      listenerRegistration = db.collection("colors")
        .addSnapshotListener { [weak self] (querySnapshot, error) in
          guard let documents = querySnapshot?.documents else {
            self?.errorMessage = "No documents in 'colors' collection"
            return
          }
          
          self?.colorEntries = documents.compactMap { queryDocumentSnapshot in
            let result = Result { try queryDocumentSnapshot.data(as: ColorEntry.self) }
            
            switch result {
            case .success(let colorEntry):
              if let colorEntry = colorEntry {
                // A ColorEntry value was successfully initialized from the DocumentSnapshot.
                self?.errorMessage = nil
                return colorEntry
              }
              else {
                // A nil value was successfully initialized from the DocumentSnapshot,
                // or the DocumentSnapshot was nil.
                self?.errorMessage = "Document doesn't exist."
                return nil
              }
            case .failure(let error):
              // A ColorEntry value could not be initialized from the DocumentSnapshot.
              switch error {
              case DecodingError.typeMismatch(_, let context):
                self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
              case DecodingError.valueNotFound(_, let context):
                self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
              case DecodingError.keyNotFound(_, let context):
                self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
              case DecodingError.dataCorrupted(let key):
                self?.errorMessage = "\(error.localizedDescription): \(key)"
              default:
                self?.errorMessage = "Error decoding document: \(error.localizedDescription)"
              }
              return nil
            }
          }
        }
    }
  }
  
  func addColorEntry() {
    let collectionRef = db.collection("colors")
    do {
      let newDocReference = try collectionRef.addDocument(from: newColor)
      print("ColorEntry stored with new document reference: \(newDocReference)")
    }
    catch {
      print(error)
    }
  }
}

ここで示したコード スニペットはすべて、こちらの GitHub リポジトリからダウンロードできるサンプル アプリケーションの一部です。

Codable を使ってみましょう

Swift の Codable API を使用すると、シリアル化された形式のデータとアプリケーションのデータモデル間でデータを強力かつ柔軟にマッピングできます。このガイドでは、データストアとして Cloud Firestore を使用するアプリでの使いやすさを確認しました。

単純なデータ型を使った基本的な例から始めて、より複雑なデータモデルに進んでいきましたが、マッピングは Codable と Firebase を使用して実装しました。

Codable の詳細については、次のリソースをご覧ください。

このガイドでは、Cloud Firestore ドキュメントのマッピングについて包括的な情報を提供することを目指しましたが、すべてが網羅されているわけではありません。ほかの方法で型のマッピングを処理している方もいらっしゃるでしょう。その場合は、下の [フィードバックを送信] ボタンを使って、Cloud Firestore データの他の型をどのようにマッピングしているのか、また、Swift でどのようにデータを表現しているのかお聞かせください。

しかし、Cloud Firestore の Codable サポートを利用しない理由はありません。一度試してみてはいかがでしょうか。