Asigna datos de Cloud Firestore con Codable de Swift

La API de Codable de Swift, que se presentó en Swift 4, nos permite aprovechar la potencia del compilador para facilitar la asignación de datos de formatos serializados a tipos de Swift.

Es posible que hayas usado Codable para asignar datos de una API web al modelo de datos de tu app (y viceversa), pero es mucho más flexible.

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

Cuando se recupera un documento de Cloud Firestore, tu app recibirá un diccionario de pares clave-valor (o un array de diccionarios, si usas una de las operaciones que muestran varios documentos).

Ahora, puedes continuar usando directamente los diccionarios en Swift, y ofrecen una gran flexibilidad que podría ser justo lo que requiere tu caso de uso. Sin embargo, este enfoque no cuenta con seguridad de tipos y, además, es fácil ingresar errores difíciles de encontrar cuando escribes errores ortográficos en los nombres de atributos o si olvidas asignar el nuevo atributo que agregó tu equipo cuando lanzó una emocionante función nueva la semana pasada.

En el pasado, muchos desarrolladores solucionaron estas deficiencias implementando de una capa de asignación simple que les permitió asignar diccionarios a los tipos de Swift. Sin embargo, la mayoría de estas implementaciones se basan en especificar de forma manual la asignación entre documentos de Cloud Firestore y los tipos correspondientes del modelo de datos de tu app.

La compatibilidad de Cloud Firestore con la API de Codable de Swift facilita mucho más este proceso:

  • Ya no tendrás que implementar manualmente ningún código de asignación.
  • Es fácil definir cómo asignar atributos con nombres diferentes.
  • Tiene compatibilidad integrada con muchos de los tipos de Swift.
  • Además, es fácil agregar compatibilidad con la asignación de tipos personalizados.
  • Lo mejor de todo es que, para los modelos de datos simples, no tendrás que escribir código de asignación.

Asigna datos

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

Esto significa que podemos usar la sintaxis de subíndice 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 puedes ver, estamos haciendo suposiciones sobre los tipos de datos de los campos del documento. Estos pueden o no ser correctos.

Recuerda que, como no hay esquema, puedes agregar fácilmente un documento nuevo a la colección y elegir un tipo diferente para un campo. Por accidente, puedes elegir una cadena para el campo numberOfPages, lo que generaría un problema de asignación difícil de encontrar. Además, deberás actualizar el código de asignación cada vez que se agregue un campo nuevo, lo que es bastante engorroso.

Y no olvidemos que, de esta forma, no estamos aprovechando el sistema estricto de tipos de Swift, que conoce exactamente el tipo correcto de cada una de las propiedades de Book.

¿Qué significa Codable?

Según la documentación de Apple, Codable es un “tipo que puede convertirse en una representación externa y dejar de ser una”. De hecho, Codable es un alias de tipo para los protocolos Encodable y Decodable. Cuando un tipo de Swift se ajusta a este protocolo, el compilador sintetiza el código necesario para codificar o 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 puedes ver, establecer el tipo en Codable es mínimamente invasivo. Solo tuvimos que agregar la conformidad con el protocolo. No se requirieron otros cambios.

Con este cambio implementado, 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)

Asigna desde y hacia tipos simples en documentos de Cloud Firestore
con Codable

Cloud Firestore admite un amplio conjunto de tipos de datos, que van desde cadenas simples hasta mapas anidados. La mayoría de estos corresponden directamente a los tipos integrados de Swift. Veamos cómo asignar algunos tipos de datos simples antes de profundizar en los más complejos.

Sigue estos pasos para asignar documentos de Cloud Firestore a tipos de Swift:

  1. Asegúrate de que agregaste el framework FirebaseFirestore a tu proyecto. Puedes usar Swift Package Manager o CocoaPods para hacerlo.
  2. Importa FirebaseFirestore a tu archivo de Swift.
  3. Adapta tu tipo a Codable.
  4. Opcional: Si quieres usar el tipo en una vista List, agrega una propiedad id a tu tipo y usa @DocumentID para indicarle a Cloud Firestore que lo asigne al ID del documento. Analizaremos este tema con más detalle a continuación.
  5. Usa documentReference.data(as: ) para asignar una referencia de documento a un tipo Swift.
  6. Usa documentReference.setData(from: ) para asignar datos de tipos de Swift a un documento de Cloud Firestore.
  7. Opcional pero muy recomendable: Implementa el manejo adecuado de errores.

Actualicemos nuestro tipo Book según corresponda:

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

Como este tipo ya se ajustaba al protocolo Codable, solo tuvimos que agregar la propiedad id y anotarla con el wrapper de la propiedad @DocumentID.

Tomando el fragmento de código anterior para recuperar y asignar un documento, podemos reemplazar todo el código de asignación manual por 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)
        }
      }
    }
  }
}

Puedes escribirlo de forma aún más concisa especificando el tipo de documento cuando llamas a getDocument(as:). Este procedimiento realizará la asignación por ti y mostrará un tipo Result que contiene el documento asignado o un error en caso de que la decodificación falle:

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 a documentReference.setData(from: ). Este es el código para guardar una instancia de Book (incluye medidas básicas de manejo de errores):

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

Cuando se agrega un documento nuevo, Cloud Firestore se encarga automáticamente de asignarle un ID de documento nuevo. Esto incluso funciona cuando la app está sin conexión.

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 asignar 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 asignar en nuestros documentos son valores simples, como el título del libro o el nombre del autor, pero ¿qué ocurre cuando necesitamos almacenar un objeto más complejo? Por ejemplo, es posible que queramos almacenar las URLs de la portada del libro en diferentes resoluciones.

La manera más sencilla de hacerlo en Cloud Firestore es usar una asignación:

Almacena un tipo personalizado anidado en un documento de Firestore

Cuando se escribe la struct de Swift correspondiente, podemos aprovechar la compatibilidad de Cloud Firestore con URLs. Cuando se almacene un campo que contenga 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?
}

Observa cómo definimos una struct CoverImages para la asignación de portada del documento de Cloud Firestore. Si marcas la propiedad de portada en BookWithCoverImages como opcional, podemos controlar el hecho de que es posible que algunos documentos no contengan un atributo de portada.

Si te interesa saber por qué no hay un fragmento de código para recuperar o actualizar datos, te alegrará saber que no necesitas ajustar el código para leer ni escribir desde Cloud Firestore: esto funciona con el código escrito en la sección inicial.

Arrays

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 Guía del autoestopista galáctico puede aparecer en varias categorías, en este caso, “Ciencia ficción” y “Comedia”:

Almacena un array en un documento de Firestore

En Cloud Firestore, podemos modelar esto con un arreglo de valores. Esto es compatible con cualquier tipo codificado (como String, Int, etcétera). A continuación, se muestra cómo agregar un array 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]
}

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

Almacena un array de tipos personalizados en un documento de Firestore

Para almacenar etiquetas de esta manera, lo único que debemos hacer es implementar una struct Tag para representar una etiqueta y hacer que se pueda codificar:

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

Y, así, podemos almacenar un array de Tags en nuestros documentos de Book.

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

Información breve sobre la asignación de los IDs de documento

Antes de comenzar a asignar más tipos, hablemos de la asignación de los IDs de documento por un momento.

Usamos el wrapper de propiedad @DocumentID en algunos de los ejemplos anteriores para asignar los IDs de nuestros documentos de Cloud Firestore a la propiedad id de nuestros tipos de Swift. Este paso es importante por varios motivos:

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

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

Cuando trabajas con tipos anidados (como el array de etiquetas en el Book de 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 independiente. Por lo tanto, no se necesita un ID de documento.

Fechas y horas

Cloud Firestore tiene un tipo de datos integrado para administrar fechas y horas, y es fácil de usar gracias a la compatibilidad de Cloud Firestore con Codable.

Veamos este documento que representa a la madre de todos los lenguajes de programación, Ada, inventado en 1843:

Almacena fechas en un documento de Firestore

Un tipo de Swift para asignar este documento podría tener el siguiente aspecto:

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

No podemos salir de esta sección sobre fechas y horas sin tener una conversación sobre @ServerTimestamp. Este wrapper de propiedad es una potencia a la hora de tratar con marcas de tiempo en tu app.

En cualquier sistema distribuido, es posible que los relojes de los sistemas individuales no estén completamente sincronizados todo el tiempo. Se podría pensar que esto no es muy importante, pero imagina las implicaciones de que un reloj esté un poco desincronizado para un sistema de comercio de valores, incluso una desviación de milisegundos podría dar como resultado una diferencia de millones de dólares al realizar un intercambio.

Cloud Firestore controla los atributos marcados con @ServerTimestamp de la siguiente manera: si el atributo es nil cuando lo almacenas (por ejemplo, mediante addDocument()), Cloud Firestore propagará el campo con el atributo marca de tiempo del servidor actual cuando se escribió en la base de datos. Si el campo no es nil cuando llamas a addDocument() o updateData(), Cloud Firestore dejará el valor del atributo intacto. De esta manera, es fácil implementar campos como createdAt y lastUpdatedAt.

Puntos geográficos

Las ubicaciones geográficas son omnipresentes en nuestras apps. Almacenarlas desbloquea una gran cantidad de funciones emocionantes. Por ejemplo, podría ser útil almacenar la ubicación de una tarea para que tu app pueda recordártela cuando llegues a un lugar de 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 o 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)

Si necesitas más información para consultar documentos por ubicación física, consulta esta guía de solución.

Enums

Es posible que las enums sean una de las funciones de lenguaje más subestimadas en Swift. Son mucho más interesantes de lo que parece. Un caso de uso común para las enums es modelar los estados discretos de un elemento. Por ejemplo, podríamos escribir una app para administrar artículos. Para hacer un seguimiento del estado de un artículo, te recomendamos que uses una enum Status:

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

Cloud Firestore no admite enums de forma nativa (es decir, no puede aplicar el conjunto de valores), pero podemos seguir usando el hecho de que las enums pueden tener asignado un tipo y elegir uno que pueda codificar. En este ejemplo, elegimos String, lo que significa que todos los valores de enum se asignarán desde y hacia la cadena cuando se almacenen en un documento de Cloud Firestore.

Además, como Swift admite valores sin procesar personalizados, podemos personalizar qué valores hacen referencia a cada caso de enum. Por ejemplo, si decidimos almacenar el caso Status.inReview como “en revisión”, podríamos actualizar la enum anterior de la siguiente manera:

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

Personaliza la asignación

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 puede ser desarrollador de Python y decidió usar minúsculas_con_guiones_bajos para todos los nombres de sus atributos.

No te preocupes: ¡Codable nos tiene cubierto!

Para casos como este, podemos usar CodingKeys. Esta es una enum que podemos agregar a una struct codificable para especificar cómo se asignarán ciertos atributos.

Considera este documento:

Un documento de Firestore con un nombre de atributo que utiliza minúsculas_con_guiones_bajos

Para asignar este documento a una struct que tiene una propiedad de nombre de tipo String, debemos agregar una enum 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
  }
}

Según la configuración predeterminada, la API de Codable usará los nombres de las propiedades de nuestros tipos de Swift para determinar los nombres de los atributos en los documentos de Cloud Firestore que estamos intentando asignar. 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 desees usar como identificador en una vista List de SwiftUI. Si no se especificara en CodingKeys, no se asignaría cuando se recuperan datos y, por lo tanto, se convertiría en nil. De esta manera, la vista List se completará con el primer documento.

Durante el proceso de asignación, se pasarán por alto las propiedades que no figuren como casos en la enum CodingKeys respectiva. En realidad, esto puede ser conveniente si queremos excluir de manera específica algunas de las propiedades de la asignación.

Por ejemplo, si queremos excluir la propiedad reasonWhyILoveThis de la asignación, lo único que debemos hacer es quitarla de la enum 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 escribir un atributo vacío en el documento de Cloud Firestore. Swift tiene la noción de opcionales para denotar la ausencia de un valor, y Cloud Firestore también admite valores null. Sin embargo, el comportamiento predeterminado para la codificación opcional que tiene un valor nil es omitirlos. @ExplicitNull nos da cierto control sobre el manejo de las opciones de Swift cuando las codificas. Marcando una propiedad opcional como @ExplicitNull, podemos indicarle a Cloud Firestore que escriba esta propiedad en el documento con un valor nulo si contiene un valor de nil.

Usa un codificador y decodificador personalizado para asignar colores

Como último tema de nuestra cobertura de la asignación de datos con Codable, presentaremos los codificadores y decodificadores personalizados. En esta sección no se abarca un tipo de datos nativo de Cloud Firestore, pero los codificadores y decodificadores personalizados son muy útiles en tus apps 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 las asignaciones entre Swift y JSON. Existen muchas soluciones, pero la mayoría de ellas se enfocan en JSON y casi todas ellas asignan colores como un diccionario anidado compuesto por sus componentes RGB.

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

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

}

Cuando se usa decoder.singleValueContainer(), podemos decodificar un String a su equivalente Color, sin tener que anidar los componentes RGBA. Además, puedes usar estos valores en la IU web de tu app sin tener que convertirlos primero.

De esta manera, podemos actualizar el código para asignar etiquetas, lo que facilita el control directo de los colores de las etiquetas en lugar de tener que asignarlos manualmente en el código de IU de nuestra app:

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

Maneja los errores

En los fragmentos de código anteriores, mantuvimos intencionalmente el manejo de errores como mínimo, pero en una app de producción, debes asegurarte de manejar cualquier error.

Este es un fragmento de código en el que se muestra cómo manejar cualquier situación de error con la que te encuentres:

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

Maneja errores en las actualizaciones en tiempo real

El fragmento de código anterior muestra cómo manejar los errores cuando se recupera un solo documento. Además de recuperar datos una vez, Cloud Firestore también admite la entrega de actualizaciones de tu app a medida que ocurren mediante los objetos de escucha de instantáneas: podemos registrar un objeto de escucha de instantáneas en una colección (o consulta) y Cloud Firestore llamará a nuestro agente de escucha cuando haya una actualización.

A continuación, se muestra un fragmento de código que muestra cómo registrar un objeto de escucha de instantáneas, asignar datos mediante Codable y manejar cualquier error que pueda ocurrir. También se muestra cómo agregar un documento nuevo a la colección. Como verás, no es necesario actualizar el array local que contenga los documentos asignados, ya que el código del objeto de escucha de instantáneas se encarga de esto.

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 que se usan en esta publicación forman parte de una aplicación de ejemplo que puedes descargar desde este repositorio de GitHub.

Comienza a usar Codable

La API de Codable de Swift proporciona una manera potente y flexible de asignar datos de los formatos serializados hacia y desde el modelo de datos de tus aplicaciones. En esta guía, viste lo fácil que es usarla en las apps que utilizan 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 que la implementación de Codable y Firebase realizaría la asignación por nosotros.

Si buscas más detalles sobre Codable, te recomendamos los siguientes recursos:

Si bien hicimos todo lo posible para crear una guía completa sobre cómo asignar documentos de Cloud Firestore, no es exhaustiva y es posible que uses otras estrategias para asignar tus tipos. Usa el botón Enviar comentarios que aparece a continuación y cuéntanos qué estrategias usas para asignar otros tipos de datos de Cloud Firestore o representar datos en Swift.

No existe una razón para no usar la compatibilidad con Codable de Cloud Firestore.