Mappa i dati di Cloud Firestore con Swift Codable

L'API Codable di Swift, introdotta in Swift 4, ci consente di sfruttare la potenza del compilatore per semplificare la mappatura dei dati dai formati serializzati ai tipi Swift.

Potresti aver utilizzato Codable per mappare i dati da un'API Web al modello dati della tua app (e viceversa), ma è molto più flessibile di così.

In questa guida esamineremo come utilizzare Codable per mappare i dati da Cloud Firestore ai tipi Swift e viceversa.

Quando recuperi un documento da Cloud Firestore, la tua app riceverà un dizionario di coppie chiave/valore (o una serie di dizionari, se utilizzi una delle operazioni che restituiscono più documenti).

Ora puoi sicuramente continuare a utilizzare direttamente i dizionari in Swift e offrono una grande flessibilità che potrebbe essere esattamente ciò che richiede il tuo caso d'uso. Tuttavia, questo approccio non è sicuro ed è facile introdurre bug difficili da rintracciare digitando erroneamente i nomi degli attributi o dimenticando di mappare il nuovo attributo aggiunto dal tuo team quando ha rilasciato quella nuova entusiasmante funzionalità la scorsa settimana.

In passato, molti sviluppatori hanno risolto questi problemi implementando un semplice livello di mappatura che consentisse loro di mappare i dizionari sui tipi Swift. Ma ancora una volta, la maggior parte di queste implementazioni si basa sulla specifica manuale della mappatura tra i documenti Cloud Firestore e i tipi corrispondenti del modello dati della tua app.

Con il supporto di Cloud Firestore per l'API Codable di Swift, tutto ciò diventa molto più semplice:

  • Non dovrai più implementare manualmente alcun codice di mappatura.
  • È facile definire come mappare gli attributi con nomi diversi.
  • Ha il supporto integrato per molti tipi di Swift.
  • Ed è facile aggiungere il supporto per la mappatura dei tipi personalizzati.
  • Meglio ancora: per modelli di dati semplici, non dovrai scrivere alcun codice di mappatura.

Dati di mappatura

Cloud Firestore archivia i dati in documenti che associano chiavi a valori. Per recuperare i dati da un singolo documento, possiamo chiamare DocumentSnapshot.data() , che restituisce un dizionario che mappa i nomi dei campi su Any : func data() -> [String : Any]? .

Ciò significa che possiamo utilizzare la sintassi degli indici di Swift per accedere a ogni singolo campo.

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

Sebbene possa sembrare semplice e facile da implementare, questo codice è fragile, difficile da mantenere e soggetto a errori.

Come puoi vedere, stiamo facendo delle ipotesi sui tipi di dati dei campi del documento. Questi potrebbero o meno essere corretti.

Ricorda, poiché non esiste uno schema, puoi facilmente aggiungere un nuovo documento alla raccolta e scegliere un tipo diverso per un campo. Potresti scegliere accidentalmente una stringa per il campo numberOfPages , il che comporterebbe un problema di mappatura difficile da trovare. Inoltre, dovrai aggiornare il codice di mappatura ogni volta che viene aggiunto un nuovo campo, il che è piuttosto complicato.

E non dimentichiamo che non stiamo sfruttando il forte sistema di tipi di Swift, che conosce esattamente il tipo corretto per ciascuna delle proprietà di Book .

Cos'è Codable, comunque?

Secondo la documentazione di Apple, Codable è "un tipo che può convertirsi in e fuori da una rappresentazione esterna". In effetti, Codable è un alias di tipo per i protocolli Encodable e Decodable. Conformando un tipo Swift a questo protocollo, il compilatore sintetizzerà il codice necessario per codificare/decodificare un'istanza di questo tipo da un formato serializzato, come JSON.

Un tipo semplice per archiviare i dati su un libro potrebbe assomigliare a questo:

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

Come puoi vedere, conformare il tipo a Codable è minimamente invasivo. Bisognava solo aggiungere la conformità al protocollo; non erano necessarie altre modifiche.

Con questo in atto, ora possiamo facilmente codificare un libro in un oggetto 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 decodifica di un oggetto JSON in un'istanza di Book funziona come segue:

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

Mappatura da e verso tipi semplici nei documenti Cloud Firestore
utilizzando Codable

Cloud Firestore supporta un'ampia gamma di tipi di dati, che vanno dalle stringhe semplici alle mappe nidificate. La maggior parte di questi corrisponde direttamente ai tipi integrati di Swift. Diamo un'occhiata alla mappatura di alcuni tipi di dati semplici prima di immergerci in quelli più complessi.

Per mappare i documenti Cloud Firestore ai tipi Swift, procedi nel seguente modo:

  1. Assicurati di aver aggiunto il framework FirebaseFirestore al tuo progetto. Puoi utilizzare Swift Package Manager o CocoaPods per farlo.
  2. Importa FirebaseFirestore nel tuo file Swift.
  3. Conforma il tuo tipo a Codable .
  4. (Facoltativo, se desideri utilizzare il tipo in una visualizzazione List ) Aggiungi una proprietà id al tuo tipo e utilizza @DocumentID per indicare a Cloud Firestore di mapparlo all'ID documento. Ne discuteremo più dettagliatamente di seguito.
  5. Utilizza documentReference.data(as: ) per mappare un riferimento al documento a un tipo Swift.
  6. Utilizza documentReference.setData(from: ) per mappare i dati dai tipi Swift a un documento Cloud Firestore.
  7. (Facoltativo, ma altamente consigliato) Implementare una corretta gestione degli errori.

Aggiorniamo di conseguenza il nostro tipo Book :

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

Poiché questo tipo era già codificabile, dovevamo solo aggiungere la proprietà id e annotarla con il wrapper della proprietà @DocumentID .

Prendendo il frammento di codice precedente per recuperare e mappare un documento, possiamo sostituire tutto il codice di mappatura manuale con una singola riga:

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

Puoi scriverlo in modo ancora più conciso specificando il tipo di documento quando chiami getDocument(as:) . Questo eseguirà la mappatura per te e restituirà un tipo Result contenente il documento mappato o un errore nel caso in cui la decodifica non sia riuscita:

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

Aggiornare un documento esistente è semplice come chiamare documentReference.setData(from: ) . Includendo alcune operazioni di base sulla gestione degli errori, ecco il codice per salvare un'istanza 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)
    }
  }
}

Quando si aggiunge un nuovo documento, Cloud Firestore si occuperà automaticamente di assegnare un nuovo ID documento al documento. Funziona anche quando l'app è attualmente 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)
  }
}

Oltre alla mappatura di tipi di dati semplici, Cloud Firestore supporta una serie di altri tipi di dati, alcuni dei quali sono tipi strutturati che puoi utilizzare per creare oggetti nidificati all'interno di un documento.

Tipi personalizzati nidificati

La maggior parte degli attributi che vogliamo mappare nei nostri documenti sono valori semplici, come il titolo del libro o il nome dell'autore. Ma che dire dei casi in cui dobbiamo archiviare un oggetto più complesso? Ad esempio, potremmo voler memorizzare gli URL della copertina del libro in diverse risoluzioni.

Il modo più semplice per farlo in Cloud Firestore è utilizzare una mappa:

Memorizzazione di un tipo personalizzato nidificato in un documento Firestore

Quando scriviamo la struttura Swift corrispondente, possiamo sfruttare il fatto che Cloud Firestore supporta gli URL: quando memorizzi un campo che contiene un URL, verrà convertito in una stringa e 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?
}

Nota come abbiamo definito una struttura, CoverImages , per la mappa di copertura nel documento Cloud Firestore. Contrassegnando la proprietà cover su BookWithCoverImages come facoltativa, siamo in grado di gestire il fatto che alcuni documenti potrebbero non contenere un attributo cover.

Se sei curioso di sapere perché non esiste uno snippet di codice per recuperare o aggiornare i dati, sarai felice di sapere che non è necessario modificare il codice per leggere o scrivere da/su Cloud Firestore: tutto questo funziona con il codice che abbiamo ho scritto nella sezione iniziale.

Array

A volte, vogliamo archiviare una raccolta di valori in un documento. I generi di un libro sono un buon esempio: un libro come Guida galattica per autostoppisti potrebbe rientrare in diverse categorie, in questo caso "Fantascienza" e "Commedia":

Memorizzazione di un array in un documento Firestore

In Cloud Firestore, possiamo modellarlo utilizzando una serie di valori. Questo è supportato per qualsiasi tipo codificabile (come String , Int , ecc.). Di seguito viene mostrato come aggiungere una serie di generi al nostro modello Book :

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

Poiché funziona con qualsiasi tipo codificabile, possiamo utilizzare anche tipi personalizzati. Immaginiamo di voler memorizzare un elenco di tag per ogni libro. Insieme al nome del tag, vorremmo memorizzare anche il colore del tag, in questo modo:

Memorizzazione di una serie di tipi personalizzati in un documento Firestore

Per memorizzare i tag in questo modo, tutto ciò che dobbiamo fare è implementare una struttura Tag per rappresentare un tag e renderlo codificabile:

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

E proprio così, possiamo memorizzare una serie di Tags nei nostri documenti Book !

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

Qualche breve parola sulla mappatura degli ID dei documenti

Prima di passare alla mappatura di altri tipi, parliamo per un momento della mappatura degli ID dei documenti.

Abbiamo utilizzato il wrapper della proprietà @DocumentID in alcuni degli esempi precedenti per mappare l'ID documento dei nostri documenti Cloud Firestore alla proprietà id dei nostri tipi Swift. Questo è importante per una serie di motivi:

  • Ci aiuta a sapere quale documento aggiornare nel caso in cui l'utente apporti modifiche locali.
  • List di SwiftUI richiede che i suoi elementi siano Identifiable per evitare che gli elementi saltino di qua e di là quando vengono inseriti.

Vale la pena sottolineare che un attributo contrassegnato come @DocumentID non verrà codificato dal codificatore di Cloud Firestore durante la riscrittura del documento. Questo perché l'ID del documento non è un attributo del documento stesso, quindi scriverlo nel documento sarebbe un errore.

Quando si lavora con tipi nidificati (come l'array di tag sul Book in un esempio precedente di questa guida), non è necessario aggiungere una proprietà @DocumentID : le proprietà nidificate fanno parte del documento Cloud Firestore e non costituiscono un documento separato. Pertanto, non hanno bisogno di un documento d'identità.

Date e orari

Cloud Firestore dispone di un tipo di dati integrato per la gestione di date e orari e, grazie al supporto di Cloud Firestore per Codable, è semplice utilizzarli.

Diamo un'occhiata a questo documento che rappresenta la madre di tutti i linguaggi di programmazione, Ada, inventato nel 1843:

Memorizzazione delle date in un documento Firestore

Un tipo Swift per mappare questo documento potrebbe assomigliare a questo:

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

Non possiamo lasciare questa sezione relativa a date e orari senza avere una conversazione su @ServerTimestamp . Questo wrapper di proprietà è un potente strumento quando si tratta di gestire i timestamp nella tua app.

In qualsiasi sistema distribuito, è probabile che gli orologi dei singoli sistemi non siano sempre completamente sincronizzati. Potresti pensare che questo non sia un grosso problema, ma immagina le implicazioni di un orologio leggermente fuori sincronia per un sistema di negoziazione azionaria: anche una deviazione di un millisecondo potrebbe comportare una differenza di milioni di dollari durante l’esecuzione di un’operazione.

Cloud Firestore gestisce gli attributi contrassegnati con @ServerTimestamp come segue: se l'attributo è nil quando lo memorizzi (utilizzando addDocument() , ad esempio), Cloud Firestore popolerà il campo con il timestamp corrente del server al momento della scrittura nel database . Se il campo non è nil quando chiami addDocument() o updateData() , Cloud Firestore lascerà inalterato il valore dell'attributo. In questo modo, è facile implementare campi come createdAt e lastUpdatedAt .

Punti geografici

Le geolocalizzazioni sono onnipresenti nelle nostre app. Molte interessanti funzionalità diventano possibili memorizzandole. Ad esempio, potrebbe essere utile memorizzare la posizione di un'attività in modo che l'app possa ricordarti un'attività quando raggiungi una destinazione.

Cloud Firestore dispone di un tipo di dati integrato, GeoPoint , che può memorizzare la longitudine e la latitudine di qualsiasi posizione. Per mappare le posizioni da/verso un documento Cloud Firestore, possiamo utilizzare il tipo GeoPoint :

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

Il tipo corrispondente in Swift è CLLocationCoordinate2D e possiamo mappare tra questi due tipi con la seguente operazione:

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

Per ulteriori informazioni su come interrogare i documenti in base alla posizione fisica, consulta questa guida alla soluzione .

Enumerazioni

Le enumerazioni sono probabilmente una delle funzionalità linguistiche più sottovalutate in Swift; c'è molto di più in loro di quanto sembri. Un caso d'uso comune per le enumerazioni è modellare gli stati discreti di qualcosa. Ad esempio, potremmo scrivere un'app per la gestione degli articoli. Per tenere traccia dello stato di un articolo, potremmo voler utilizzare un'enumerazione Status :

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

Cloud Firestore non supporta le enumerazioni in modo nativo (ovvero, non può applicare l'insieme di valori), ma possiamo comunque sfruttare il fatto che le enumerazioni possono essere digitate e scegliere un tipo codificabile. In questo esempio, abbiamo scelto String , il che significa che tutti i valori enum verranno mappati su/da stringa quando archiviati in un documento Cloud Firestore.

E, poiché Swift supporta valori grezzi personalizzati, possiamo anche personalizzare quali valori si riferiscono a quale caso enum. Quindi, ad esempio, se decidessimo di memorizzare il caso Status.inReview come "in revisione", potremmo semplicemente aggiornare l'enumerazione precedente come segue:

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

Personalizzazione della mappatura

A volte, i nomi degli attributi dei documenti Cloud Firestore che vogliamo mappare non corrispondono ai nomi delle proprietà nel nostro modello dati in Swift. Ad esempio, uno dei nostri colleghi potrebbe essere uno sviluppatore Python e ha deciso di scegliere snake_case per tutti i nomi degli attributi.

Non preoccuparti: Codable ci copre!

Per casi come questi, possiamo utilizzare CodingKeys . Questa è un'enumerazione che possiamo aggiungere a una struttura codificabile per specificare come verranno mappati determinati attributi.

Considera questo documento:

Un documento Firestore con il nome dell'attributo snake_cased

Per mappare questo documento a una struttura che ha una proprietà name di tipo String , dobbiamo aggiungere un'enumerazione CodingKeys alla struttura ProgrammingLanguage e specificare il nome dell'attributo nel 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
  }
}

Per impostazione predefinita, l'API Codable utilizzerà i nomi delle proprietà dei nostri tipi Swift per determinare i nomi degli attributi sui documenti Cloud Firestore che stiamo tentando di mappare. Pertanto, finché i nomi degli attributi corrispondono, non è necessario aggiungere CodingKeys ai nostri tipi codificabili. Tuttavia, una volta utilizzato CodingKeys per un tipo specifico, dobbiamo aggiungere tutti i nomi di proprietà che vogliamo mappare.

Nello snippet di codice sopra, abbiamo definito una proprietà id che potremmo voler utilizzare come identificatore in una visualizzazione List SwiftUI. Se non lo specificassimo in CodingKeys , non verrebbe mappato durante il recupero dei dati e quindi diventerebbe nil . Ciò comporterebbe il riempimento della visualizzazione List con il primo documento.

Qualsiasi proprietà non elencata come caso nella rispettiva enumerazione CodingKeys verrà ignorata durante il processo di mappatura. Questo può effettivamente essere utile se vogliamo escludere specificamente alcune proprietà dalla mappatura.

Quindi, ad esempio, se vogliamo escludere la proprietà reasonWhyILoveThis dalla mappatura, tutto ciò che dobbiamo fare è rimuoverla dall'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
  }
}

Occasionalmente potremmo voler riscrivere un attributo vuoto nel documento Cloud Firestore. Swift utilizza il concetto di opzioni opzionali per denotare l'assenza di un valore e Cloud Firestore supporta anche valori null . Tuttavia, il comportamento predefinito per la codifica degli elementi opzionali che hanno un valore nil è semplicemente ometterli. @ExplicitNull ci dà un certo controllo su come vengono gestiti gli optional di Swift durante la codifica: contrassegnando una proprietà opzionale come @ExplicitNull , possiamo dire a Cloud Firestore di scrivere questa proprietà nel documento con un valore null se contiene un valore nil .

Utilizzo di un codificatore e decodificatore personalizzato per la mappatura dei colori

Come ultimo argomento nella nostra trattazione dei dati di mappatura con Codable, introduciamo codificatori e decodificatori personalizzati. Questa sezione non copre un tipo di dati Cloud Firestore nativo, ma i codificatori e decodificatori personalizzati sono ampiamente utili nelle tue app Cloud Firestore.

"Come posso mappare i colori" è una delle domande più frequenti degli sviluppatori, non solo per Cloud Firestore, ma anche per la mappatura tra Swift e JSON. Esistono molte soluzioni in circolazione, ma la maggior parte si concentra su JSON e quasi tutte mappano i colori come un dizionario annidato composto dai suoi componenti RGB.

Sembra che dovrebbe esserci una soluzione migliore e più semplice. Perché non utilizziamo i colori web (o, per essere più specifici, la notazione esadecimale dei colori CSS): sono facili da usare (essenzialmente solo una stringa) e supportano persino la trasparenza!

Per poter mappare uno Swift Color al suo valore esadecimale, dobbiamo creare un'estensione Swift che aggiunga 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)
  }

}

Utilizzando decoder.singleValueContainer() , possiamo decodificare una String nel suo equivalente Color , senza dover annidare i componenti RGBA. Inoltre, puoi utilizzare questi valori nell'interfaccia utente web della tua app, senza doverli prima convertire!

In questo modo possiamo aggiornare il codice per la mappatura dei tag, semplificando la gestione diretta dei colori dei tag invece di doverli mappare manualmente nel codice dell'interfaccia utente della nostra 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]
}

Gestione degli errori

Negli snippet di codice sopra riportati abbiamo intenzionalmente mantenuto la gestione degli errori al minimo, ma in un'app di produzione ti consigliamo di assicurarti di gestire con garbo eventuali errori.

Ecco uno snippet di codice che mostra come gestire eventuali situazioni di errore in cui potresti imbatterti:

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

Gestione degli errori negli aggiornamenti in tempo reale

Il frammento di codice precedente mostra come gestire gli errori durante il recupero di un singolo documento. Oltre a recuperare i dati una volta, Cloud Firestore supporta anche la distribuzione degli aggiornamenti alla tua app non appena si verificano, utilizzando i cosiddetti ascoltatori di istantanee: possiamo registrare un ascoltatore di istantanee su una raccolta (o query) e Cloud Firestore chiamerà il nostro ascoltatore ogni volta che è un aggiornamento.

Ecco uno snippet di codice che mostra come registrare un listener di snapshot, mappare i dati utilizzando Codable e gestire eventuali errori che potrebbero verificarsi. Mostra anche come aggiungere un nuovo documento alla raccolta. Come vedrai, non è necessario aggiornare noi stessi l'array locale che contiene i documenti mappati, poiché di questo si occupa il codice nel listener dello 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)
    }
  }
}

Tutti i frammenti di codice utilizzati in questo post fanno parte di un'applicazione di esempio che puoi scaricare da questo repository GitHub .

Vai avanti e usa Codable!

L'API Codable di Swift fornisce un modo potente e flessibile per mappare i dati dai formati serializzati da e verso il modello dati delle tue applicazioni. In questa guida hai visto quanto sia facile da utilizzare nelle app che utilizzano Cloud Firestore come archivio dati.

Partendo da un esempio di base con tipi di dati semplici, abbiamo progressivamente aumentato la complessità del modello di dati, potendo fare affidamento sull'implementazione di Codable e Firebase per eseguire la mappatura per noi.

Per maggiori dettagli su Codable, consiglio le seguenti risorse:

Anche se abbiamo fatto del nostro meglio per compilare una guida completa per la mappatura dei documenti Cloud Firestore, questa non è esaustiva e potresti utilizzare altre strategie per mappare i tuoi tipi. Utilizzando il pulsante Invia feedback di seguito, facci sapere quali strategie utilizzi per mappare altri tipi di dati Cloud Firestore o rappresentare dati in Swift.

Non c'è davvero alcun motivo per non utilizzare il supporto Codable di Cloud Firestore.