Сопоставление данных Cloud Firestore с помощью Swift Codable

Codable API, представленный в Swift 4, позволяет использовать возможности компилятора, чтобы упростить сопоставление данных из сериализованных форматов с типами Swift.

Возможно, вы использовали Codable для сопоставления данных из веб-API с моделью данных вашего приложения (и наоборот), но он гораздо более гибкий.

В этом руководстве мы рассмотрим, как использовать Codable для сопоставления данных из Cloud Firestore с типами Swift и наоборот.

При получении документа из Cloud Firestore ваше приложение получит словарь пар "ключ-значение" (или массив словарей, если вы используете одну из операций, возвращающих несколько документов).

Конечно, вы можете продолжать использовать словари в Swift, и они предлагают большую гибкость, которая может быть именно тем, что требуется в вашем случае. Однако этот подход не является типобезопасным, и в нем легко допустить трудно отслеживаемые ошибки, неправильно написав названия атрибутов или забыв сопоставить новый атрибут, добавленный вашей командой при выпуске новой функции на прошлой неделе.

В прошлом многие разработчики обходили эти недостатки, реализуя простой слой сопоставления, который позволял им сопоставлять словари с типами Swift. Но опять же, большинство этих реализаций основаны на ручном указании сопоставления между документами Cloud Firestore и соответствующими типами модели данных вашего приложения.

Благодаря поддержке Codable API в Cloud Firestore это стало намного проще:

  • Вам больше не нужно вручную реализовывать код сопоставления.
  • Вы можете легко сопоставить атрибуты с разными названиями.
  • В нем есть встроенная поддержка многих типов Swift.
  • Кроме того, вы можете легко добавить поддержку сопоставления пользовательских типов.
  • Для простых моделей данных вам даже не придется писать код сопоставления.

Данные сопоставления

Cloud Firestore хранит данные в документах, которые сопоставляют ключи со значениями. Чтобы получить данные из отдельного документа, мы можем вызвать DocumentSnapshot.data(), который возвращает словарь, сопоставляющий имена полей с Any: func data() -> [String : Any]?.

Это означает, что мы можем использовать синтаксис нижнего индекса Swift для доступа к каждому отдельному полю.

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, что приведет к проблеме сопоставления, которую будет трудно обнаружить. Кроме того, вам придется обновлять код сопоставления при каждом добавлении нового поля, что довольно неудобно.

И не забывайте, что мы не используем преимущества строгой системы типов Swift, которая точно знает правильный тип для каждого из свойств Book.

Что такое 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)

Сопоставление с простыми типами в документах Cloud Firestore с помощью Codable

Cloud Firestore поддерживает широкий набор типов данных – от простых строк до вложенных карт. Большинство из них напрямую соответствуют встроенным типам Swift. Давайте сначала рассмотрим сопоставление некоторых простых типов данных, прежде чем переходить к более сложным.

Чтобы сопоставить документы Cloud Firestore с типами Swift, выполните следующие действия:

  1. Убедитесь, что вы добавили фреймворк FirebaseFirestore в проект. Для этого можно использовать Swift Package Manager или CocoaPods.
  2. Импортируйте FirebaseFirestore в файл Swift.
  3. Приведите тип к Codable.
  4. (Необязательно, если вы хотите использовать тип в представлении List.) Добавьте свойство id в тип и используйте @DocumentID, чтобы указать 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
}

Поскольку этот тип уже был кодируемым, нам нужно было только добавить свойство id и аннотировать его с помощью оболочки свойства @DocumentID.

В предыдущем фрагменте кода для получения и сопоставления документа мы можем заменить весь код ручного сопоставления одной строкой:

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)
    }
  }
}

При добавлении нового документа Cloud Firestore автоматически присвоит ему идентификатор. Это работает, даже если приложение в данный момент находится в автономном режиме.

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?
}

Обратите внимание, как мы определили структуру CoverImages для карты покрытия в документе Cloud Firestore. Пометив свойство cover в BookWithCoverImages как необязательное, мы сможем обрабатывать документы, в которых нет атрибута cover.

Если вам интересно, почему нет фрагмента кода для получения или обновления данных, то вы будете рады узнать, что нет необходимости корректировать код для чтения или записи из/в Cloud Firestore: все это работает с кодом, который мы написали в начальном разделе.

Массивы

Иногда нам нужно сохранить в документе набор значений. Хороший пример – жанры книги. Книга Автостопом по галактике может относиться к нескольким категориям, в данном случае – "Научная фантастика" и "Комедия".

Хранение массива в документе Firestore

В Cloud Firestore это можно смоделировать с помощью массива значений. Поддерживаются любые кодируемые типы, например String, Int и т. д. Ниже показано, как добавить массив жанров в модель Book.

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

Поскольку это работает для любого кодируемого типа, мы также можем использовать пользовательские типы. Представьте, что мы хотим хранить список тегов для каждой книги. Вместе с названием тега мы также хотим сохранить его цвет, например:

Хранение массива специальных типов в документе Firestore

Чтобы хранить теги таким образом, нам нужно реализовать структуру Tag, которая будет представлять тег и делать его кодируемым:

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]
}

Что нужно знать о сопоставлении идентификаторов документов

Прежде чем перейти к сопоставлению других типов, давайте немного поговорим о сопоставлении идентификаторов документов.

В некоторых из предыдущих примеров мы использовали оболочку свойства @DocumentID, чтобы сопоставить идентификатор документа наших документов Cloud Firestore со свойством id наших типов Swift. Это важно по нескольким причинам:

  • Это помогает нам определить, какой документ нужно обновить, если пользователь вносит локальные изменения.
  • Элементы List в SwiftUI должны быть Identifiable, чтобы они не перемещались при вставке.

Стоит отметить, что атрибут, помеченный как @DocumentID, не будет закодирован кодировщиком Cloud Firestore при обратной записи документа. Это связано с тем, что идентификатор документа не является атрибутом самого документа, поэтому его запись в документ будет ошибкой.

При работе с вложенными типами (например, массивом тегов в Book в предыдущем примере) добавлять свойство @DocumentID не требуется: вложенные свойства являются частью документа Cloud Firestore и не составляют отдельный документ. Поэтому им не нужен идентификатор документа.

Даты и время

В Cloud Firestore есть встроенный тип данных для обработки дат и времени, и благодаря поддержке Codable в Cloud Firestore их легко использовать.

Давайте посмотрим на этот документ, который представляет собой мать всех языков программирования, Аду, изобретенную в 1843 году:

Хранение дат в документе Firestore

Тип Swift для сопоставления этого документа может выглядеть следующим образом:

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

Мы не можем закончить этот раздел о датах и времени, не поговорив о @ServerTimestamp. Эта оболочка свойства – мощный инструмент для работы с временными метками в приложении.

В любой распределенной системе часы на отдельных устройствах могут быть не полностью синхронизированы. Вы можете подумать, что это не имеет большого значения, но представьте себе последствия того, что часы немного не синхронизированы для системы торговли акциями: даже отклонение в миллисекунду может привести к разнице в миллионы долларов при совершении сделки.

Cloud Firestore обрабатывает атрибуты, помеченные как @ServerTimestamp, следующим образом: если атрибут равен nil, когда вы его сохраняете (например, с помощью addDocument()), Cloud Firestore заполнит поле текущей серверной меткой времени в момент записи в базу данных. Если при вызове addDocument() или updateData() поле не равно nil, Cloud Firestore не изменит значение атрибута. Это позволяет легко реализовать такие поля, как createdAt и lastUpdatedAt.

Географические точки

Геолокация используется во многих приложениях. Многие интересные функции становятся возможными благодаря их хранению. Например, для задачи может быть полезно сохранить местоположение, чтобы приложение могло напомнить вам о задаче, когда вы достигнете пункта назначения.

В Cloud Firestore есть встроенный тип данных GeoPoint, который может хранить долготу и широту любого местоположения. Чтобы сопоставить местоположения из документа Cloud Firestore или с ним, можно использовать тип GeoPoint:

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

Соответствующий тип в Swift – CLLocationCoordinate2D. Мы можем сопоставить эти два типа с помощью следующей операции:

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 не поддерживает перечисления (то есть не может применять набор значений), но мы можем использовать тот факт, что перечисления могут быть типизированы, и выбрать кодируемый тип. В этом примере мы выбрали String, что означает, что все значения перечисления будут сопоставляться со строкой и из нее при хранении в документе Cloud Firestore.

А поскольку Swift поддерживает пользовательские необработанные значения, мы можем даже настроить, какие значения относятся к какому случаю перечисления. Например, если мы решим хранить статус Status.inReview как "на рассмотрении", мы можем просто обновить приведенное выше перечисление следующим образом:

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

Настройка сопоставления

Иногда названия атрибутов документов Cloud Firestore, которые мы хотим сопоставить, не совпадают с названиями свойств в нашей модели данных в Swift. Например, один из наших коллег может быть разработчиком Python и решил выбрать snake_case для всех названий атрибутов.

Не волнуйтесь, Codable нам поможет!

В таких случаях можно использовать CodingKeys. Это перечисление, которое можно добавить в кодируемую структуру, чтобы указать, как будут сопоставляться определенные атрибуты.

Рассмотрим следующий документ:

Документ Firestore с названием атрибута в змеином регистре

Чтобы сопоставить этот документ со структурой, у которой есть свойство name типа String, нужно добавить перечисление 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, которые мы пытаемся сопоставить. Поэтому, пока названия атрибутов совпадают, нет необходимости добавлять CodingKeys к нашим кодируемым типам. Однако если мы используем CodingKeys для определенного типа, нам нужно добавить все названия свойств, которые мы хотим сопоставить.

В приведенном выше фрагменте кода мы определили свойство id, которое можно использовать в качестве идентификатора в представлении List SwiftUI. Если бы мы не указали его в 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, мы можем указать Cloud Firestore записать это свойство в документ с нулевым значением, если оно содержит значение nil.

Использование пользовательского кодировщика и декодера для сопоставления цветов

В заключение нашего разговора о сопоставлении данных с помощью Codable давайте рассмотрим пользовательские кодировщики и декодеры. В этом разделе не рассматривается нативный тип данных Cloud Firestore, но пользовательские кодировщики и декодеры широко используются в приложениях Cloud Firestore.

"Как сопоставить цвета?" – один из самых частых вопросов разработчиков, причем не только по Cloud Firestore, но и по сопоставлению между Swift и JSON. Существует множество решений, но большинство из них ориентированы на JSON, и почти все они сопоставляют цвета как вложенный словарь, состоящий из его компонентов RGB.

Кажется, должно быть лучшее и более простое решение. Почему бы не использовать веб-цвета (или, точнее, шестнадцатеричный код цвета CSS) — они просты в использовании (по сути, просто строка) и даже поддерживают прозрачность!

Чтобы сопоставить Swift Color с его шестнадцатеричным значением, нам нужно создать расширение Swift, которое добавляет Codable в Color.

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() мы можем декодировать String в эквивалент Color без необходимости вкладывать компоненты RGBA. Кроме того, вы можете использовать эти значения в веб-интерфейсе приложения без предварительного преобразования.

Теперь мы можем обновить код для сопоставления тегов, чтобы упростить управление цветами тегов напрямую, а не сопоставлять их вручную в коде пользовательского интерфейса нашего приложения:

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 также поддерживает доставку обновлений в приложение по мере их появления с помощью так называемых прослушивателей снимков. Мы можем зарегистрировать прослушиватель снимков в коллекции (или запросе), и 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!

Codable API в Swift предоставляет мощный и гибкий способ сопоставления данных из сериализованных форматов с моделью данных вашего приложения и обратно. В этом руководстве вы узнали, как легко использовать его в приложениях, которые используют Cloud Firestore в качестве хранилища данных.

Начиная с базового примера с простыми типами данных, мы постепенно увеличивали сложность модели данных, при этом полагаясь на реализацию Codable и Firebase для выполнения сопоставления за нас.

Дополнительную информацию о Codable можно найти в следующих ресурсах:

Мы постарались составить максимально полное руководство по сопоставлению документов Cloud Firestore, но оно не является исчерпывающим, и вы можете использовать другие стратегии для сопоставления типов. Нажмите кнопку Отправить отзыв ниже и расскажите, какие стратегии вы используете для сопоставления других типов данных Cloud Firestore или представления данных в Swift.

Нет никаких причин не использовать поддержку Codable в Cloud Firestore.