Mapeie dados do Cloud Firestore com Swift Codable

A API Codable do Swift, introduzida no Swift 4, nos permite aproveitar o poder do compilador para facilitar o mapeamento de dados de formatos serializados para tipos Swift.

Você pode estar usando Codable para mapear dados de uma API web para o modelo de dados do seu aplicativo (e vice-versa), mas é muito mais flexível do que isso.

Neste guia, veremos como Codable pode ser usado para mapear dados do Cloud Firestore para tipos Swift e vice-versa.

Ao buscar um documento no Cloud Firestore, seu aplicativo receberá um dicionário de pares chave/valor (ou uma matriz de dicionários, se você usar uma das operações que retorna vários documentos).

Agora, você certamente pode continuar a usar dicionários diretamente no Swift, e eles oferecem uma grande flexibilidade que pode ser exatamente o que seu caso de uso exige. No entanto, essa abordagem não é segura e é fácil introduzir bugs difíceis de rastrear, digitando nomes de atributos incorretamente ou esquecendo de mapear o novo atributo que sua equipe adicionou quando lançou esse novo recurso interessante na semana passada.

No passado, muitos desenvolvedores contornaram essas deficiências implementando uma camada de mapeamento simples que lhes permitia mapear dicionários para tipos Swift. Mas, novamente, a maioria dessas implementações se baseia na especificação manual do mapeamento entre os documentos do Cloud Firestore e os tipos correspondentes do modelo de dados do seu aplicativo.

Com o suporte do Cloud Firestore para a API Codable do Swift, isso se torna muito mais fácil:

  • Você não precisará mais implementar manualmente nenhum código de mapeamento.
  • É fácil definir como mapear atributos com nomes diferentes.
  • Possui suporte integrado para muitos dos tipos do Swift.
  • E é fácil adicionar suporte para mapeamento de tipos personalizados.
  • O melhor de tudo: para modelos de dados simples, você não precisará escrever nenhum código de mapeamento.

Mapeamento de dados

O Cloud Firestore armazena dados em documentos que mapeiam chaves para valores. Para buscar dados de um documento individual, podemos chamar DocumentSnapshot.data() , que retorna um dicionário mapeando os nomes dos campos para Any : func data() -> [String : Any]? .

Isso significa que podemos usar a sintaxe subscrita do Swift para acessar 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)
      }
    }
  }
}

Embora possa parecer simples e fácil de implementar, esse código é frágil, difícil de manter e sujeito a erros.

Como você pode ver, estamos fazendo suposições sobre os tipos de dados dos campos do documento. Isso pode ou não estar correto.

Lembre-se, como não há esquema, você pode facilmente adicionar um novo documento à coleção e escolher um tipo diferente para um campo. Você pode acidentalmente escolher uma string para o campo numberOfPages , o que resultaria em um problema de mapeamento difícil de encontrar. Além disso, você terá que atualizar seu código de mapeamento sempre que um novo campo for adicionado, o que é bastante complicado.

E não vamos esquecer que não estamos aproveitando o sistema de tipos fortes do Swift, que conhece exatamente o tipo correto para cada uma das propriedades de Book .

Afinal, o que é codificável?

De acordo com a documentação da Apple, Codable é “um tipo que pode se converter dentro e fora de uma representação externa”. Na verdade, Codable é um alias de tipo para os protocolos Encodable e Decodable. Ao conformar um tipo Swift a este protocolo, o compilador sintetizará o código necessário para codificar/decodificar uma instância deste tipo a partir de um formato serializado, como JSON.

Um tipo simples para armazenar dados sobre um livro pode ser assim:

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

Como você pode ver, adaptar o tipo ao Codable é minimamente invasivo. Só tivemos que adicionar a conformidade ao protocolo; nenhuma outra alteração foi necessária.

Com isso implementado, agora podemos codificar facilmente um livro em um 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)")
}

A decodificação de um objeto JSON para uma instância Book funciona da seguinte maneira:

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

Mapeamento de e para tipos simples em documentos do Cloud Firestore
usando Codificável

O Cloud Firestore oferece suporte a um amplo conjunto de tipos de dados, desde strings simples até mapas aninhados. A maioria deles corresponde diretamente aos tipos integrados do Swift. Vamos dar uma olhada no mapeamento de alguns tipos de dados simples antes de nos aprofundarmos nos mais complexos.

Para mapear documentos do Cloud Firestore para tipos Swift, siga estas etapas:

  1. Certifique-se de ter adicionado a estrutura FirebaseFirestore ao seu projeto. Você pode usar o Swift Package Manager ou CocoaPods para fazer isso.
  2. Importe FirebaseFirestore para o seu arquivo Swift.
  3. Combine seu tipo com Codable .
  4. (Opcional, se você quiser usar o tipo em uma visualização List ) Adicione uma propriedade id ao seu tipo e use @DocumentID para instruir o Cloud Firestore a mapeá-lo para o ID do documento. Discutiremos isso com mais detalhes abaixo.
  5. Use documentReference.data(as: ) para mapear uma referência de documento para um tipo Swift.
  6. Use documentReference.setData(from: ) para mapear dados de tipos Swift para um documento do Cloud Firestore.
  7. (Opcional, mas altamente recomendado) Implemente o tratamento de erros adequado.

Vamos atualizar nosso tipo Book de acordo:

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

Como esse tipo já era codificável, só tivemos que adicionar a propriedade id e anotá-la com o wrapper da propriedade @DocumentID .

Pegando o trecho de código anterior para buscar e mapear um documento, podemos substituir todo o código de mapeamento manual por uma única linha:

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

Você pode escrever isso de forma ainda mais concisa especificando o tipo do documento ao chamar getDocument(as:) . Isso executará o mapeamento para você e retornará um tipo Result contendo o documento mapeado ou um erro caso a decodificação falhe:

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

Atualizar um documento existente é tão simples quanto chamar documentReference.setData(from: ) . Incluindo algum tratamento básico de erros, aqui está o código para salvar uma instância 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)
    }
  }
}

Ao adicionar um novo documento, o Cloud Firestore se encarregará automaticamente de atribuir um novo ID de documento ao documento. Isso funciona mesmo quando o aplicativo está offline.

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

Além de mapear tipos de dados simples, o Cloud Firestore oferece suporte a vários outros tipos de dados, alguns dos quais são tipos estruturados que você pode usar para criar objetos aninhados dentro de um documento.

Tipos personalizados aninhados

A maioria dos atributos que queremos mapear em nossos documentos são valores simples, como o título do livro ou o nome do autor. Mas e aqueles casos em que precisamos armazenar um objeto mais complexo? Por exemplo, podemos querer armazenar os URLs da capa do livro em diferentes resoluções.

A maneira mais fácil de fazer isso no Cloud Firestore é usar um mapa:

Armazenar um tipo personalizado aninhado em um documento do Firestore

Ao escrever a estrutura Swift correspondente, podemos aproveitar o fato de que o Cloud Firestore suporta URLs — ao armazenar um campo que contém uma URL, ele será convertido em uma string e vice-versa:

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 como definimos uma estrutura, CoverImages , para o mapa de cobertura no documento Cloud Firestore. Marcando a propriedade cover em BookWithCoverImages como opcional, podemos lidar com o fato de que alguns documentos podem não conter um atributo cover.

Se você está curioso para saber por que não há snippet de código para buscar ou atualizar dados, ficará satisfeito em saber que não há necessidade de ajustar o código para leitura ou gravação de/para o Cloud Firestore: tudo isso funciona com o código que nós escrevi na seção inicial.

Matrizes

Às vezes, queremos armazenar uma coleção de valores em um documento. Os gêneros de um livro são um bom exemplo: um livro como O Guia do Mochileiro das Galáxias pode se enquadrar em várias categorias – neste caso, “Ficção Científica” e “Comédia”:

Armazenando um array em um documento do Firestore

No Cloud Firestore, podemos modelar isso usando uma matriz de valores. Isso é compatível com qualquer tipo codificável (como String , Int , etc.). A seguir mostramos como adicionar uma variedade de gêneros ao nosso modelo Book :

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

Como isso funciona para qualquer tipo codificável, também podemos usar tipos personalizados. Imagine que queremos armazenar uma lista de tags para cada livro. Junto com o nome da tag, gostaríamos de armazenar também a cor da tag, assim:

Armazenando uma variedade de tipos personalizados em um documento do Firestore

Para armazenar tags dessa maneira, tudo o que precisamos fazer é implementar uma estrutura Tag para representar uma tag e torná-la codificável:

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

E assim, podemos armazenar uma série de Tags nos documentos do nosso Book !

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

Uma palavra rápida sobre mapeamento de IDs de documentos

Antes de passarmos ao mapeamento de mais tipos, vamos falar um pouco sobre o mapeamento de IDs de documentos.

Usamos o wrapper de propriedade @DocumentID em alguns dos exemplos anteriores para mapear o ID do documento de nossos documentos do Cloud Firestore para a propriedade id de nossos tipos Swift. Isso é importante por vários motivos:

  • Ajuda-nos a saber qual documento atualizar caso o usuário faça alterações locais.
  • List do SwiftUI exige que seus elementos sejam Identifiable ​​para evitar que os elementos saltem quando são inseridos.

Vale ressaltar que um atributo marcado como @DocumentID não será codificado pelo codificador do Cloud Firestore ao escrever o documento de volta. Isso ocorre porque o ID do documento não é um atributo do documento em si — portanto, escrevê-lo no documento seria um erro.

Ao trabalhar com tipos aninhados (como a matriz de tags no Book em um exemplo anterior deste guia), não é necessário adicionar uma propriedade @DocumentID : as propriedades aninhadas fazem parte do documento do Cloud Firestore e não constituem um documento separado. Portanto, eles não precisam de um documento de identificação.

Datas e horários

O Cloud Firestore possui um tipo de dados integrado para lidar com datas e horas e, graças ao suporte do Cloud Firestore para Codable, é fácil usá-los.

Vejamos este documento que representa a mãe de todas as linguagens de programação, Ada, inventada em 1843:

Armazenando datas em um documento do Firestore

Um tipo Swift para mapear este documento pode ser assim:

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

Não podemos sair desta seção sobre datas e horários sem conversar sobre @ServerTimestamp . Esse wrapper de propriedade é uma potência quando se trata de lidar com carimbos de data/hora em seu aplicativo.

Em qualquer sistema distribuído, é provável que os relógios dos sistemas individuais não estejam completamente sincronizados o tempo todo. Você pode pensar que isso não é grande coisa, mas imagine as implicações de um relógio ligeiramente fora de sincronia para um sistema de negociação de ações: mesmo um desvio de milissegundos pode resultar em uma diferença de milhões de dólares ao executar uma negociação.

O Cloud Firestore trata atributos marcados com @ServerTimestamp da seguinte forma: se o atributo for nil quando você o armazena (usando addDocument() , por exemplo), o Cloud Firestore preencherá o campo com o carimbo de data/hora atual do servidor no momento de gravá-lo no banco de dados . Se o campo não for nil quando você chamar addDocument() ou updateData() , o Cloud Firestore deixará o valor do atributo inalterado. Dessa forma, é fácil implementar campos como createdAt e lastUpdatedAt .

Geopontos

As geolocalizações são onipresentes em nossos aplicativos. Muitos recursos interessantes tornam-se possíveis ao armazená-los. Por exemplo, pode ser útil armazenar um local para uma tarefa para que seu aplicativo possa lembrá-lo sobre uma tarefa quando você chegar a um destino.

O Cloud Firestore possui um tipo de dados integrado, GeoPoint , que pode armazenar a longitude e a latitude de qualquer local. Para mapear locais de/para um documento do Cloud Firestore, podemos usar o tipo GeoPoint :

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

O tipo correspondente em Swift é CLLocationCoordinate2D e podemos mapear entre esses dois tipos com a seguinte operação:

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

Para saber mais sobre como consultar documentos por localização física, confira este guia de soluções .

Enums

Enums são provavelmente um dos recursos de linguagem mais subestimados em Swift; há muito mais neles do que aparenta. Um caso de uso comum para enums é modelar os estados discretos de algo. Por exemplo, podemos estar escrevendo um aplicativo para gerenciar artigos. Para rastrear o status de um artigo, podemos usar um enum Status :

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

O Cloud Firestore não oferece suporte nativo a enums (ou seja, não pode impor o conjunto de valores), mas ainda podemos aproveitar o fato de que enums podem ser digitados e escolher um tipo codificável. Neste exemplo, escolhemos String , o que significa que todos os valores enum serão mapeados de/para string quando armazenados em um documento do Cloud Firestore.

E, como o Swift suporta valores brutos personalizados, podemos até personalizar quais valores se referem a qual caso de enum. Por exemplo, se decidirmos armazenar o caso Status.inReview como "em revisão", poderíamos simplesmente atualizar a enumeração acima da seguinte forma:

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

Personalizando o mapeamento

Às vezes, os nomes dos atributos dos documentos do Cloud Firestore que queremos mapear não correspondem aos nomes das propriedades em nosso modelo de dados em Swift. Por exemplo, um de nossos colegas de trabalho pode ser desenvolvedor Python e decidiu escolher Snake_case para todos os nomes de atributos.

Não se preocupe: Codable nos ajuda!

Para casos como esses, podemos fazer uso de CodingKeys . Este é um enum que podemos adicionar a uma estrutura codificável para especificar como certos atributos serão mapeados.

Considere este documento:

Um documento do Firestore com um nome de atributo Snake_cased

Para mapear este documento para uma estrutura que possui uma propriedade name do tipo String , precisamos adicionar uma enumeração CodingKeys à estrutura ProgrammingLanguage e especificar o nome do atributo no 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
  }
}

Por padrão, a API Codable usará os nomes das propriedades dos nossos tipos Swift para determinar os nomes dos atributos nos documentos do Cloud Firestore que estamos tentando mapear. Portanto, desde que os nomes dos atributos correspondam, não há necessidade de adicionar CodingKeys aos nossos tipos codificáveis. No entanto, uma vez que usamos CodingKeys para um tipo específico, precisamos adicionar todos os nomes de propriedades que queremos mapear.

No trecho de código acima, definimos uma propriedade id que podemos querer usar como identificador em uma visualização List SwiftUI. Se não o especificássemos em CodingKeys , ele não seria mapeado ao buscar dados e, portanto, se tornaria nil . Isso resultaria no preenchimento da visualização List com o primeiro documento.

Qualquer propriedade que não esteja listada como um caso na respectiva enumeração CodingKeys será ignorada durante o processo de mapeamento. Na verdade, isso pode ser conveniente se quisermos especificamente excluir algumas das propriedades do mapeamento.

Então, por exemplo, se quisermos excluir a propriedade reasonWhyILoveThis do mapeamento, tudo o que precisamos fazer é removê-la da enumeração 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
  }
}

Ocasionalmente, podemos querer escrever um atributo vazio de volta no documento do Cloud Firestore. Swift tem a noção de opcionais para denotar a ausência de um valor, e o Cloud Firestore também oferece suporte a valores null . No entanto, o comportamento padrão para codificar opcionais que possuem valor nil é simplesmente omiti-los. @ExplicitNull nos dá algum controle sobre como os opcionais do Swift são tratados ao codificá-los: sinalizando uma propriedade opcional como @ExplicitNull , podemos dizer ao Cloud Firestore para gravar essa propriedade no documento com um valor nulo se ele contiver um valor nil .

Usando um codificador e decodificador personalizado para mapear cores

Como último tópico em nossa cobertura de mapeamento de dados com Codable, vamos apresentar codificadores e decodificadores personalizados. Esta seção não aborda um tipo de dados nativo do Cloud Firestore, mas codificadores e decodificadores personalizados são amplamente úteis em seus aplicativos do Cloud Firestore.

"Como posso mapear cores" é uma das perguntas mais frequentes dos desenvolvedores, não apenas para Cloud Firestore, mas também para mapeamento entre Swift e JSON. Existem muitas soluções por aí, mas a maioria delas se concentra em JSON e quase todas mapeiam cores como um dicionário aninhado composto por seus componentes RGB.

Parece que deveria haver uma solução melhor e mais simples. Por que não usamos cores da web (ou, para ser mais específico, notação de cores hexadecimais CSS) - elas são fáceis de usar (essencialmente apenas uma string) e até suportam transparência!

Para poder mapear um Swift Color para seu valor hexadecimal, precisamos criar uma extensão Swift que adicione 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)
  }

}

Usando decoder.singleValueContainer() , podemos decodificar uma String em seu equivalente Color , sem ter que aninhar os componentes RGBA. Além disso, você pode usar esses valores na interface web do seu aplicativo, sem precisar convertê-los primeiro!

Com isso, podemos atualizar o código para mapeamento de tags, facilitando o manuseio direto das cores das tags, em vez de ter que mapeá-las manualmente no código da UI do nosso aplicativo:

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

Tratamento de erros

Nos trechos de código acima, mantivemos intencionalmente o tratamento de erros no mínimo, mas em um aplicativo de produção, você deve certificar-se de lidar com quaisquer erros com elegância.

Aqui está um trecho de código que mostra como lidar com qualquer situação de erro que você possa encontrar:

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

Tratamento de erros em atualizações ao vivo

O trecho de código anterior demonstra como lidar com erros ao buscar um único documento. Além de buscar dados uma vez, o Cloud Firestore também oferece suporte ao fornecimento de atualizações para seu aplicativo conforme elas acontecem, usando os chamados ouvintes de instantâneo: podemos registrar um ouvinte de instantâneo em uma coleção (ou consulta), e o Cloud Firestore ligará para nosso ouvinte sempre que houver é uma atualização.

Aqui está um trecho de código que mostra como registrar um ouvinte de snapshot, mapear dados usando Codable e lidar com quaisquer erros que possam ocorrer. Também mostra como adicionar um novo documento à coleção. Como você verá, não há necessidade de atualizarmos nós mesmos o array local que contém os documentos mapeados, pois isso é feito pelo código no ouvinte de snapshot.

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 os trechos de código usados ​​nesta postagem fazem parte de um aplicativo de exemplo que você pode baixar neste repositório GitHub .

Vá em frente e use Codable!

A API Codable do Swift fornece uma maneira poderosa e flexível de mapear dados de formatos serializados de e para o modelo de dados de seus aplicativos. Neste guia, você viu como é fácil utilizá-lo em apps que utilizam o Cloud Firestore como datastore.

Partindo de um exemplo básico com tipos de dados simples, aumentamos progressivamente a complexidade do modelo de dados, podendo ao mesmo tempo contar com a implementação do Codable e do Firebase para realizar o mapeamento para nós.

Para mais detalhes sobre Codable, recomendo os seguintes recursos:

Embora tenhamos feito o possível para compilar um guia abrangente para mapear documentos do Cloud Firestore, ele não é completo e você pode estar usando outras estratégias para mapear seus tipos. Usando o botão Enviar feedback abaixo, informe-nos quais estratégias você usa para mapear outros tipos de dados do Cloud Firestore ou representar dados em Swift.

Realmente não há razão para não usar o suporte Codable do Cloud Firestore.