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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Cloud Firestore обрабатывает атрибуты, отмеченные @ServerTimestamp , следующим образом: если атрибут равен nil при его сохранении (например, с помощью addDocument() ), Cloud Firestore заполнит поле текущей меткой времени сервера на момент записи его в базу данных. Если поле не равно nil при вызове addDocument() или updateData() , 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_case

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

По умолчанию API Codable будет использовать имена свойств наших типов Swift для определения имен атрибутов в документах Cloud Firestore , которые мы пытаемся сопоставить. Поэтому, пока имена атрибутов совпадают, нет необходимости добавлять CodingKeys к нашим типам codable. Однако, как только мы используем 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) — они просты в использовании (по сути, это просто строка), и они даже поддерживают прозрачность!

Чтобы иметь возможность сопоставить Color Swift с его шестнадцатеричным значением, нам нужно создать расширение 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!

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

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

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

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

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