Mapee datos de Cloud Firestore con Swift Codable

La API codificable de Swift, introducida en Swift 4, nos permite aprovechar el poder del compilador para facilitar la asignación de datos de formatos serializados a tipos Swift.

Es posible que haya estado utilizando Codable para asignar datos de una API web al modelo de datos de su aplicación (y viceversa), pero es mucho más flexible que eso.

En esta guía, veremos cómo se puede usar Codable para asignar datos de Cloud Firestore a tipos Swift y viceversa.

Al recuperar un documento de Cloud Firestore, su aplicación recibirá un diccionario de pares clave/valor (o una serie de diccionarios, si usa una de las operaciones que devuelven varios documentos).

Ahora, ciertamente puede continuar usando diccionarios directamente en Swift, y ofrecen una gran flexibilidad que podría ser exactamente lo que requiere su caso de uso. Sin embargo, este enfoque no es seguro para escribir y es fácil introducir errores difíciles de rastrear al escribir mal los nombres de los atributos u olvidarse de asignar el nuevo atributo que su equipo agregó cuando lanzaron esa nueva y emocionante característica la semana pasada.

En el pasado, muchos desarrolladores solucionaron estas deficiencias implementando una capa de mapeo simple que les permitió mapear diccionarios a tipos Swift. Pero nuevamente, la mayoría de estas implementaciones se basan en especificar manualmente la asignación entre los documentos de Cloud Firestore y los tipos correspondientes del modelo de datos de su aplicación.

Con el soporte de Cloud Firestore para la API Codable de Swift, esto se vuelve mucho más fácil:

  • Ya no tendrá que implementar manualmente ningún código de mapeo.
  • Es fácil definir cómo asignar atributos con diferentes nombres.
  • Tiene soporte integrado para muchos de los tipos de Swift.
  • Y es fácil agregar soporte para mapear tipos personalizados.
  • Lo mejor de todo: para modelos de datos simples, no tendrá que escribir ningún código de mapeo.

Datos cartográficos

Cloud Firestore almacena datos en documentos que asignan claves a valores. Para recuperar datos de un documento individual, podemos llamar a DocumentSnapshot.data() , que devuelve un diccionario que asigna los nombres de los campos a Any : func data() -> [String : Any]? .

Esto significa que podemos usar la sintaxis de subíndices de Swift para acceder a cada campo individual.

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

Si bien puede parecer sencillo y fácil de implementar, este código es frágil, difícil de mantener y propenso a errores.

Como puede ver, estamos haciendo suposiciones sobre los tipos de datos de los campos del documento. Estos pueden ser correctos o no.

Recuerde, como no existe un esquema, puede agregar fácilmente un nuevo documento a la colección y elegir un tipo diferente para un campo. Podrías elegir accidentalmente una cadena para el campo numberOfPages , lo que provocaría un problema de asignación difícil de encontrar. Además, tendrás que actualizar tu código de mapeo cada vez que se agregue un nuevo campo, lo cual es bastante engorroso.

Y no olvidemos que no estamos aprovechando el sólido sistema de tipos de Swift, que conoce exactamente el tipo correcto para cada una de las propiedades de Book .

¿Qué es codificable, de todos modos?

Según la documentación de Apple, Codable es "un tipo que puede convertirse en y fuera de una representación externa". De hecho, Codable es un alias de tipo para los protocolos Encodable y Decodable. Al adaptar un tipo Swift a este protocolo, el compilador sintetizará el código necesario para codificar/decodificar una instancia de este tipo a partir de un formato serializado, como JSON.

Un tipo simple para almacenar datos sobre un libro podría verse así:

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

Como puede ver, ajustar el tipo a Codable es mínimamente invasivo. Sólo nos quedó agregar la conformidad al protocolo; no se requirieron otros cambios.

Una vez implementado esto, ahora podemos codificar fácilmente un libro en un objeto 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)")
}

La decodificación de un objeto JSON en una instancia Book funciona de la siguiente manera:

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

Mapeo hacia y desde tipos simples en documentos de Cloud Firestore
usando codificable

Cloud Firestore admite un amplio conjunto de tipos de datos, desde cadenas simples hasta mapas anidados. La mayoría de estos corresponden directamente a los tipos integrados de Swift. Primero echemos un vistazo al mapeo de algunos tipos de datos simples antes de sumergirnos en los más complejos.

Para asignar documentos de Cloud Firestore a tipos Swift, siga estos pasos:

  1. Asegúrate de haber agregado el marco FirebaseFirestore a tu proyecto. Puede utilizar Swift Package Manager o CocoaPods para hacerlo.
  2. Importe FirebaseFirestore a su archivo Swift.
  3. Conforme su tipo a Codable .
  4. (Opcional, si desea usar el tipo en una vista List ) Agregue una propiedad id a su tipo y use @DocumentID para indicarle a Cloud Firestore que asigne esto al ID del documento. Discutiremos esto con más detalle a continuación.
  5. Utilice documentReference.data(as: ) para asignar una referencia de documento a un tipo Swift.
  6. Utilice documentReference.setData(from: ) para asignar datos de tipos Swift a un documento de Cloud Firestore.
  7. (Opcional, pero muy recomendado) Implemente un manejo adecuado de errores.

Actualicemos nuestro tipo Book en consecuencia:

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

Como este tipo ya era codificable, solo tuvimos que agregar la propiedad id y anotarla con el contenedor de propiedad @DocumentID .

Tomando el fragmento de código anterior para buscar y mapear un documento, podemos reemplazar todo el código de mapeo manual con una sola línea:

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

Puede escribir esto de manera aún más concisa especificando el tipo de documento al llamar getDocument(as:) . Esto realizará la asignación por usted y devolverá un tipo Result que contiene el documento asignado, o un error en caso de que falle la decodificación:

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

Actualizar un documento existente es tan simple como llamar documentReference.setData(from: ) . Incluyendo algo de manejo de errores básico, aquí está el código para guardar una instancia 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)
    }
  }
}

Al agregar un nuevo documento, Cloud Firestore se encargará automáticamente de asignarle una nueva ID de documento. Esto funciona incluso cuando la aplicación está actualmente fuera de línea.

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

Además de mapear tipos de datos simples, Cloud Firestore admite otros tipos de datos, algunos de los cuales son tipos estructurados que puedes usar para crear objetos anidados dentro de un documento.

Tipos personalizados anidados

La mayoría de los atributos que queremos mapear en nuestros documentos son valores simples, como el título del libro o el nombre del autor. Pero ¿qué pasa con esos casos en los que necesitamos almacenar un objeto más complejo? Por ejemplo, es posible que deseemos almacenar las URL de la portada del libro en diferentes resoluciones.

La forma más sencilla de hacer esto en Cloud Firestore es utilizar un mapa:

Almacenar un tipo personalizado anidado en un documento de Firestore

Al escribir la estructura Swift correspondiente, podemos aprovechar el hecho de que Cloud Firestore admite URL: al almacenar un campo que contiene una URL, se convertirá en una cadena y viceversa:

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

Observe cómo definimos una estructura, CoverImages , para el mapa de portada en el documento de Cloud Firestore. Al marcar la propiedad de portada en BookWithCoverImages como opcional, podemos manejar el hecho de que algunos documentos pueden no contener un atributo de portada.

Si tiene curiosidad por saber por qué no hay un fragmento de código para recuperar o actualizar datos, le complacerá saber que no es necesario ajustar el código para leer o escribir desde/hacia Cloud Firestore: todo esto funciona con el código que He escrito en la sección inicial.

matrices

A veces queremos almacenar una colección de valores en un documento. Los géneros de un libro son un buen ejemplo: un libro como La guía del autoestopista galáctico podría clasificarse en varias categorías (en este caso, "ciencia ficción" y "comedia"):

Almacenar una matriz en un documento de Firestore

En Cloud Firestore, podemos modelar esto usando una matriz de valores. Esto es compatible con cualquier tipo codificable (como String , Int , etc.). A continuación se muestra cómo agregar una variedad de géneros a nuestro modelo Book :

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

Dado que esto funciona para cualquier tipo codificable, también podemos usar tipos personalizados. Imaginemos que queremos almacenar una lista de etiquetas para cada libro. Junto con el nombre de la etiqueta, también nos gustaría almacenar el color de la etiqueta, así:

Almacenar una variedad de tipos personalizados en un documento de Firestore

Para almacenar etiquetas de esta manera, todo lo que necesitamos hacer es implementar una estructura Tag para representar una etiqueta y hacerla codificable:

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

¡Y así, podemos almacenar una variedad de Tags en los documentos de nuestro Book !

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

Unas breves palabras sobre el mapeo de ID de documentos

Antes de pasar a mapear más tipos, hablemos por un momento sobre mapear ID de documentos.

Usamos el contenedor de propiedad @DocumentID en algunos de los ejemplos anteriores para asignar el ID del documento de nuestros documentos de Cloud Firestore a la propiedad id de nuestros tipos Swift. Esto es importante por varias razones:

  • Nos ayuda a saber qué documento actualizar en caso de que el usuario realice cambios locales.
  • List de SwiftUI requiere que sus elementos sean Identifiable para evitar que los elementos salten cuando se insertan.

Vale la pena señalar que un atributo marcado como @DocumentID no será codificado por el codificador de Cloud Firestore al volver a escribir el documento. Esto se debe a que el ID del documento no es un atributo del documento en sí, por lo que escribirlo en el documento sería un error.

Cuando se trabaja con tipos anidados (como la matriz de etiquetas en el Book en un ejemplo anterior de esta guía), no es necesario agregar una propiedad @DocumentID : las propiedades anidadas son parte del documento de Cloud Firestore y no constituyen un documento separado. Por lo tanto, no necesitan un documento de identificación.

Fechas y horarios

Cloud Firestore tiene un tipo de datos integrado para manejar fechas y horas y, gracias al soporte de Cloud Firestore para Codable, es sencillo usarlos.

Echemos un vistazo a este documento que representa la madre de todos los lenguajes de programación, Ada, inventada en 1843:

Almacenamiento de fechas en un documento de Firestore

Un tipo Swift para mapear este documento podría verse así:

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

No podemos dejar esta sección sobre fechas y horas sin tener una conversación sobre @ServerTimestamp . Este contenedor de propiedades es una potencia cuando se trata de lidiar con marcas de tiempo en su aplicación.

En cualquier sistema distribuido, es probable que los relojes de los sistemas individuales no estén completamente sincronizados todo el tiempo. Podría pensar que esto no es gran cosa, pero imagine las implicaciones de un reloj ligeramente desincronizado para un sistema de comercio de acciones: incluso una desviación de un milisegundo podría resultar en una diferencia de millones de dólares al ejecutar una operación.

Cloud Firestore maneja los atributos marcados con @ServerTimestamp de la siguiente manera: si el atributo es nil cuando lo almacena (usando addDocument() , por ejemplo), Cloud Firestore completará el campo con la marca de tiempo actual del servidor al momento de escribirlo en la base de datos. . Si el campo no es nil cuando llamas addDocument() o updateData() , Cloud Firestore dejará el valor del atributo intacto. De esta manera, es fácil implementar campos como createdAt y lastUpdatedAt .

Geopuntos

Las geolocalizaciones son omnipresentes en nuestras aplicaciones. Muchas funciones interesantes son posibles almacenándolas. Por ejemplo, podría resultar útil almacenar una ubicación para una tarea para que su aplicación pueda recordarle una tarea cuando llegue a un destino.

Cloud Firestore tiene un tipo de datos integrado, GeoPoint , que puede almacenar la longitud y latitud de cualquier ubicación. Para asignar ubicaciones desde/hacia un documento de Cloud Firestore, podemos usar el tipo GeoPoint :

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

El tipo correspondiente en Swift es CLLocationCoordinate2D y podemos asignar entre esos dos tipos con la siguiente operación:

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

Para obtener más información sobre cómo consultar documentos por ubicación física, consulte esta guía de soluciones .

Enumeraciones

Las enumeraciones son probablemente una de las características del lenguaje más subestimadas en Swift; hay mucho más en ellos de lo que parece. Un caso de uso común para las enumeraciones es modelar los estados discretos de algo. Por ejemplo, podríamos estar escribiendo una aplicación para gestionar artículos. Para realizar un seguimiento del estado de un artículo, es posible que deseemos utilizar una enumeración Status :

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

Cloud Firestore no admite enumeraciones de forma nativa (es decir, no puede imponer el conjunto de valores), pero aún podemos aprovechar el hecho de que las enumeraciones se pueden escribir y elegir un tipo codificable. En este ejemplo, elegimos String , lo que significa que todos los valores de enumeración se asignarán a/desde una cadena cuando se almacenen en un documento de Cloud Firestore.

Y, dado que Swift admite valores sin procesar personalizados, podemos incluso personalizar qué valores se refieren a qué caso de enumeración. Entonces, por ejemplo, si decidimos almacenar el caso Status.inReview como "en revisión", podríamos simplemente actualizar la enumeración anterior de la siguiente manera:

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

Personalizando el mapeo

A veces, los nombres de los atributos de los documentos de Cloud Firestore que queremos asignar no coinciden con los nombres de las propiedades en nuestro modelo de datos en Swift. Por ejemplo, uno de nuestros compañeros de trabajo podría ser desarrollador de Python y decidió elegir Snake_case para todos sus nombres de atributos.

No se preocupe: ¡Codable nos tiene cubiertos!

Para casos como estos, podemos hacer uso de CodingKeys . Esta es una enumeración que podemos agregar a una estructura codificable para especificar cómo se asignarán ciertos atributos.

Considere este documento:

Un documento de Firestore con un nombre de atributo Snake_cased

Para asignar este documento a una estructura que tiene una propiedad de nombre de tipo String , necesitamos agregar una enumeración CodingKeys a la estructura ProgrammingLanguage y especificar el nombre del atributo en el documento:

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

De forma predeterminada, la API Codable utilizará los nombres de propiedad de nuestros tipos Swift para determinar los nombres de los atributos en los documentos de Cloud Firestore que intentamos asignar. Entonces, siempre que los nombres de los atributos coincidan, no es necesario agregar CodingKeys a nuestros tipos codificables. Sin embargo, una vez que usamos CodingKeys para un tipo específico, debemos agregar todos los nombres de propiedades que queremos asignar.

En el fragmento de código anterior, definimos una propiedad id que quizás queramos usar como identificador en una vista List de SwiftUI. Si no lo especificamos en CodingKeys , no se asignará al recuperar datos y, por lo tanto, se volverá nil . Esto daría como resultado que la vista List se llenara con el primer documento.

Cualquier propiedad que no aparezca como un caso en la enumeración CodingKeys respectiva se ignorará durante el proceso de asignación. En realidad, esto puede ser conveniente si queremos excluir específicamente algunas de las propiedades del mapeo.

Entonces, por ejemplo, si queremos excluir la propiedad reasonWhyILoveThis del mapeo, todo lo que debemos hacer es eliminarla de la enumeración 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
  }
}

En ocasiones, es posible que queramos volver a escribir un atributo vacío en el documento de Cloud Firestore. Swift tiene la noción de opciones para indicar la ausencia de un valor y Cloud Firestore también admite valores null . Sin embargo, el comportamiento predeterminado para codificar opciones que tienen un valor nil es simplemente omitirlas. @ExplicitNull nos brinda cierto control sobre cómo se manejan las opciones de Swift al codificarlas: al marcar una propiedad opcional como @ExplicitNull , podemos decirle a Cloud Firestore que escriba esta propiedad en el documento con un valor nulo si contiene un valor de nil .

Usar un codificador y decodificador personalizado para mapear colores

Como último tema en nuestra cobertura de datos de mapeo con Codable, presentemos codificadores y decodificadores personalizados. Esta sección no cubre un tipo de datos nativo de Cloud Firestore, pero los codificadores y decodificadores personalizados son muy útiles en tus aplicaciones de Cloud Firestore.

"¿Cómo puedo asignar colores?" es una de las preguntas más frecuentes de los desarrolladores, no solo para Cloud Firestore, sino también para el mapeo entre Swift y JSON. Existen muchas soluciones, pero la mayoría se centra en JSON y casi todas asignan colores como un diccionario anidado compuesto por sus componentes RGB.

Parece que debería haber una solución mejor y más sencilla. ¿Por qué no usamos colores web (o, para ser más específicos, notación de color hexadecimal CSS)? Son fáciles de usar (esencialmente solo una cadena) e incluso admiten transparencia.

Para poder asignar un Color Swift a su valor hexadecimal, necesitamos crear una extensión Swift que agregue Codable to 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)
  }

}

Al usar decoder.singleValueContainer() , podemos decodificar una String a su Color equivalente, sin tener que anidar los componentes RGBA. Además, puedes usar estos valores en la interfaz de usuario web de tu aplicación, ¡sin tener que convertirlos primero!

Con esto, podemos actualizar el código para asignar etiquetas, lo que facilita el manejo de los colores de las etiquetas directamente en lugar de tener que asignarlos manualmente en el código de interfaz de usuario de nuestra aplicación:

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

Errores de manejo

En los fragmentos de código anteriores, mantuvimos intencionalmente el manejo de errores al mínimo, pero en una aplicación de producción, querrás asegurarte de manejar cualquier error con elegancia.

Aquí hay un fragmento de código que muestra cómo manejar cualquier situación de error con la que pueda encontrarse:

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

Manejo de errores en actualizaciones en vivo

El fragmento de código anterior muestra cómo manejar errores al recuperar un solo documento. Además de recuperar datos una vez, Cloud Firestore también admite la entrega de actualizaciones a su aplicación a medida que ocurren, utilizando los llamados oyentes de instantáneas: podemos registrar un oyente de instantáneas en una colección (o consulta), y Cloud Firestore llamará a nuestro oyente cuando haya es una actualización.

Aquí hay un fragmento de código que muestra cómo registrar un detector de instantáneas, asignar datos usando Codable y manejar cualquier error que pueda ocurrir. También muestra cómo agregar un nuevo documento a la colección. Como verá, no es necesario actualizar nosotros mismos la matriz local que contiene los documentos asignados, ya que de esto se encarga el código en el detector de instantáneas.

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

Todos los fragmentos de código utilizados en esta publicación son parte de una aplicación de muestra que puede descargar desde este repositorio de GitHub .

¡Adelante y usa Codable!

La API codificable de Swift proporciona una forma potente y flexible de asignar datos desde formatos serializados hacia y desde el modelo de datos de su aplicación. En esta guía, vio lo fácil que es usarlo en aplicaciones que usan Cloud Firestore como almacén de datos.

A partir de un ejemplo básico con tipos de datos simples, aumentamos progresivamente la complejidad del modelo de datos, y al mismo tiempo pudimos confiar en la implementación de Codable y Firebase para realizar el mapeo por nosotros.

Para obtener más detalles sobre Codable, recomiendo los siguientes recursos:

Aunque hicimos todo lo posible para compilar una guía completa para mapear documentos de Cloud Firestore, esto no es exhaustivo y es posible que estés usando otras estrategias para mapear tus tipos. Utilizando el botón Enviar comentarios a continuación, infórmenos qué estrategias utiliza para mapear otros tipos de datos de Cloud Firestore o representar datos en Swift.

Realmente no hay ninguna razón para no utilizar el soporte Codable de Cloud Firestore.