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

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

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

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

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

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

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

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

  • Вам больше не придется вручную реализовывать какой-либо код сопоставления.
  • Легко определить, как сопоставлять атрибуты с разными именами.
  • Он имеет встроенную поддержку многих типов 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 и Decodeable. Соответствуя типу 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 и обратно.
используя кодируемый

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

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

  1. Убедитесь, что вы добавили в свой проект платформу FirebaseFirestore . Для этого вы можете использовать диспетчер пакетов Swift или 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. Пометив свойство обложки в BookWithCoverImages как необязательное, мы можем справиться с тем фактом, что некоторые документы могут не содержать атрибут обложки.

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

Давайте взглянем на этот документ, который представляет собой мать всех языков программирования, Аду, изобретенную в 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 с именем атрибута Snake_cased.

Чтобы сопоставить этот документ со структурой, имеющей свойство 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 записать это свойство в документ со значением null, если оно содержит значение 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!

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

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

Для получения более подробной информации о Codable я рекомендую следующие ресурсы:

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

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