Cartographier les données Cloud Firestore avec Swift Codable

L'API Codable de Swift, introduite dans Swift 4, nous permet d'exploiter la puissance du compilateur pour faciliter le mappage des données des formats sérialisés vers les types Swift.

Vous avez peut-être utilisé Codable pour mapper les données d'une API Web au modèle de données de votre application (et vice versa), mais c'est beaucoup plus flexible que cela.

Dans ce guide, nous allons voir comment Codable peut être utilisé pour mapper les données de Cloud Firestore aux types Swift et vice versa.

Lors de la récupération d'un document depuis Cloud Firestore, votre application recevra un dictionnaire de paires clé/valeur (ou un tableau de dictionnaires, si vous utilisez l'une des opérations renvoyant plusieurs documents).

Désormais, vous pouvez certainement continuer à utiliser directement les dictionnaires dans Swift, et ils offrent une grande flexibilité qui pourrait correspondre exactement à ce que nécessite votre cas d'utilisation. Cependant, cette approche n'est pas sécurisée et il est facile d'introduire des bogues difficiles à détecter en orthographiant mal les noms d'attributs ou en oubliant de mapper le nouvel attribut ajouté par votre équipe lors de la livraison de cette nouvelle fonctionnalité passionnante la semaine dernière.

Dans le passé, de nombreux développeurs ont contourné ces lacunes en implémentant une simple couche de mappage qui leur permettait de mapper des dictionnaires sur des types Swift. Mais encore une fois, la plupart de ces implémentations reposent sur la spécification manuelle du mappage entre les documents Cloud Firestore et les types correspondants du modèle de données de votre application.

Avec la prise en charge par Cloud Firestore de l'API Codable de Swift, cela devient beaucoup plus facile :

  • Vous n’aurez plus à implémenter manuellement de code de mappage.
  • Il est facile de définir comment mapper des attributs portant des noms différents.
  • Il prend en charge de nombreux types de Swift.
  • Et il est facile d'ajouter la prise en charge du mappage de types personnalisés.
  • Mieux encore : pour les modèles de données simples, vous n'aurez pas besoin d'écrire de code de mappage.

Données cartographiques

Cloud Firestore stocke les données dans des documents qui mappent les clés aux valeurs. Pour récupérer les données d'un document individuel, nous pouvons appeler DocumentSnapshot.data() , qui renvoie un dictionnaire mappant les noms de champs à un Any : func data() -> [String : Any]? .

Cela signifie que nous pouvons utiliser la syntaxe d'indice de Swift pour accéder à chaque champ individuel.

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

Bien qu'il puisse sembler simple et facile à mettre en œuvre, ce code est fragile, difficile à maintenir et sujet aux erreurs.

Comme vous pouvez le constater, nous faisons des hypothèses sur les types de données des champs du document. Ceux-ci pourraient être corrects ou non.

N'oubliez pas qu'en l'absence de schéma, vous pouvez facilement ajouter un nouveau document à la collection et choisir un type différent pour un champ. Vous pourriez accidentellement choisir une chaîne pour le champ numberOfPages , ce qui entraînerait un problème de mappage difficile à trouver. De plus, vous devrez mettre à jour votre code de mappage chaque fois qu'un nouveau champ est ajouté, ce qui est plutôt fastidieux.

Et n'oublions pas que nous ne profitons pas du système de types puissant de Swift, qui connaît exactement le type correct pour chacune des propriétés de Book .

Au fait, qu’est-ce que Codable ?

Selon la documentation d'Apple, Codable est "un type qui peut se convertir en et hors d'une représentation externe". En fait, Codable est un alias de type pour les protocoles Encodable et Decodable. En conformant un type Swift à ce protocole, le compilateur synthétisera le code nécessaire pour encoder/décoder une instance de ce type à partir d'un format sérialisé, tel que JSON.

Un type simple de stockage de données sur un livre pourrait ressembler à ceci :

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

Comme vous pouvez le constater, la conformité du type à Codable est peu invasive. Il suffisait d'ajouter la conformité au protocole ; aucun autre changement n’était nécessaire.

Une fois cela en place, nous pouvons désormais facilement encoder un livre dans un objet 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)")
}

Le décodage d'un objet JSON vers une instance Book fonctionne comme suit :

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

Mappage vers et depuis des types simples dans les documents Cloud Firestore
en utilisant Codable

Cloud Firestore prend en charge un large éventail de types de données, allant des simples chaînes aux cartes imbriquées. La plupart d'entre eux correspondent directement aux types intégrés de Swift. Jetons d'abord un coup d'œil à la cartographie de certains types de données simples avant de nous plonger dans les plus complexes.

Pour mapper des documents Cloud Firestore à des types Swift, procédez comme suit :

  1. Assurez-vous d'avoir ajouté le framework FirebaseFirestore à votre projet. Vous pouvez utiliser Swift Package Manager ou CocoaPods pour ce faire.
  2. Importez FirebaseFirestore dans votre fichier Swift.
  3. Conformez votre type à Codable .
  4. (Facultatif, si vous souhaitez utiliser le type dans une vue List ) Ajoutez une propriété id à votre type et utilisez @DocumentID pour indiquer à Cloud Firestore de la mapper à l'ID du document. Nous en discuterons plus en détail ci-dessous.
  5. Utilisez documentReference.data(as: ) pour mapper une référence de document à un type Swift.
  6. Utilisez documentReference.setData(from: ) pour mapper les données des types Swift à un document Cloud Firestore.
  7. (Facultatif, mais fortement recommandé) Implémentez une gestion appropriée des erreurs.

Mettons à jour notre type Book en conséquence :

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

Comme ce type était déjà codable, il suffisait d'ajouter la propriété id et de l'annoter avec le wrapper de propriété @DocumentID .

En prenant l'extrait de code précédent pour récupérer et mapper un document, nous pouvons remplacer tout le code de mappage manuel par une seule ligne :

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

Vous pouvez écrire cela de manière encore plus concise en spécifiant le type du document lors de l'appel getDocument(as:) . Cela effectuera le mappage pour vous et renverra un type Result contenant le document mappé, ou une erreur en cas d'échec du décodage :

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

Mettre à jour un document existant est aussi simple que d’appeler documentReference.setData(from: ) . Y compris une gestion des erreurs de base, voici le code pour enregistrer une instance 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)
    }
  }
}

Lors de l'ajout d'un nouveau document, Cloud Firestore se chargera automatiquement d'attribuer un nouvel ID de document au document. Cela fonctionne même lorsque l'application est actuellement hors ligne.

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

En plus du mappage de types de données simples, Cloud Firestore prend en charge un certain nombre d'autres types de données, dont certains sont des types structurés que vous pouvez utiliser pour créer des objets imbriqués dans un document.

Types personnalisés imbriqués

La plupart des attributs que nous souhaitons cartographier dans nos documents sont des valeurs simples, telles que le titre du livre ou le nom de l'auteur. Mais qu’en est-il des cas où nous devons stocker un objet plus complexe ? Par exemple, nous pourrions souhaiter stocker les URL de la couverture du livre dans différentes résolutions.

Le moyen le plus simple de procéder dans Cloud Firestore consiste à utiliser une carte :

Stockage d'un type personnalisé imbriqué dans un document Firestore

Lors de l'écriture de la structure Swift correspondante, nous pouvons profiter du fait que Cloud Firestore prend en charge les URL : lors du stockage d'un champ contenant une URL, celle-ci sera convertie en chaîne et 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?
}

Remarquez comment nous avons défini une structure, CoverImages , pour la carte de couverture dans le document Cloud Firestore. En marquant la propriété cover sur BookWithCoverImages comme facultative, nous sommes en mesure de gérer le fait que certains documents peuvent ne pas contenir d'attribut cover.

Si vous êtes curieux de savoir pourquoi il n'y a pas d'extrait de code pour récupérer ou mettre à jour les données, vous serez heureux d'apprendre qu'il n'est pas nécessaire d'ajuster le code pour lire ou écrire depuis/vers Cloud Firestore : tout cela fonctionne avec le code que nous avons J'ai écrit dans la section initiale.

Tableaux

Parfois, nous souhaitons stocker une collection de valeurs dans un document. Les genres d'un livre en sont un bon exemple : un livre comme Le Guide du voyageur galactique peut se diviser en plusieurs catégories, en l'occurrence « Science-Fiction » et « Comédie » :

Stocker un tableau dans un document Firestore

Dans Cloud Firestore, nous pouvons modéliser cela à l'aide d'un tableau de valeurs. Ceci est pris en charge pour tout type codable (tel que String , Int , etc.). Ce qui suit montre comment ajouter un éventail de genres à notre modèle Book :

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

Puisque cela fonctionne pour n’importe quel type codable, nous pouvons également utiliser des types personnalisés. Imaginez que nous souhaitions stocker une liste de balises pour chaque livre. En plus du nom de la balise, nous aimerions également stocker la couleur de la balise, comme ceci :

Stockage d'un tableau de types personnalisés dans un document Firestore

Pour stocker les balises de cette manière, tout ce que nous avons à faire est d'implémenter une structure Tag pour représenter une balise et la rendre codable :

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

Et juste comme ça, nous pouvons stocker un tableau de Tags dans nos documents Book !

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

Un petit mot sur le mappage des identifiants de documents

Avant de passer au mappage d’autres types, parlons un instant du mappage des ID de document.

Nous avons utilisé le wrapper de propriété @DocumentID dans certains des exemples précédents pour mapper l'ID de document de nos documents Cloud Firestore à la propriété id de nos types Swift. Ceci est important pour plusieurs raisons :

  • Cela nous aide à savoir quel document mettre à jour au cas où l'utilisateur apporterait des modifications locales.
  • List de SwiftUI nécessite que ses éléments soient Identifiable afin d'empêcher les éléments de sauter lorsqu'ils sont insérés.

Il convient de souligner qu'un attribut marqué comme @DocumentID ne sera pas encodé par l'encodeur de Cloud Firestore lors de la réécriture du document. En effet, l'ID du document n'est pas un attribut du document lui-même ; l'écrire dans le document serait donc une erreur.

Lorsque vous travaillez avec des types imbriqués (tels que le tableau de balises sur le Book dans un exemple précédent de ce guide), il n'est pas nécessaire d'ajouter une propriété @DocumentID : les propriétés imbriquées font partie du document Cloud Firestore et ne constituent pas un document distinct. Par conséquent, ils n’ont pas besoin d’un identifiant de document.

Dates et heures

Cloud Firestore dispose d'un type de données intégré pour gérer les dates et les heures, et grâce à la prise en charge de Codable par Cloud Firestore, il est simple de les utiliser.

Jetons un coup d'œil à ce document qui représente la mère de tous les langages de programmation, Ada, inventé en 1843 :

Stockage des dates dans un document Firestore

Un type Swift pour mapper ce document pourrait ressembler à ceci :

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

Nous ne pouvons pas quitter cette section sur les dates et heures sans avoir une conversation sur @ServerTimestamp . Ce wrapper de propriété est une centrale électrique lorsqu'il s'agit de gérer les horodatages dans votre application.

Dans tout système distribué, il est probable que les horloges des systèmes individuels ne soient pas toujours complètement synchronisées. Vous pensez peut-être que ce n’est pas grave, mais imaginez les implications d’une horloge légèrement désynchronisée pour un système de négociation d’actions : même un écart d’une milliseconde peut entraîner une différence de plusieurs millions de dollars lors de l’exécution d’une transaction.

Cloud Firestore gère les attributs marqués avec @ServerTimestamp comme suit : si l'attribut est nil lorsque vous le stockez (en utilisant addDocument() , par exemple), Cloud Firestore remplira le champ avec l'horodatage actuel du serveur au moment de son écriture dans la base de données. . Si le champ n'est pas nil lorsque vous appelez addDocument() ou updateData() , Cloud Firestore laissera la valeur de l'attribut intacte. De cette façon, il est facile d'implémenter des champs createdAt et lastUpdatedAt .

Géopoints

Les géolocalisations sont omniprésentes dans nos applications. De nombreuses fonctionnalités intéressantes deviennent possibles en les stockant. Par exemple, il peut être utile de stocker un emplacement pour une tâche afin que votre application puisse vous rappeler une tâche lorsque vous atteignez une destination.

Cloud Firestore dispose d'un type de données intégré, GeoPoint , qui peut stocker la longitude et la latitude de n'importe quel emplacement. Pour mapper des emplacements depuis/vers un document Cloud Firestore, nous pouvons utiliser le type GeoPoint :

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

Le type correspondant dans Swift est CLLocationCoordinate2D , et nous pouvons mapper entre ces deux types avec l'opération suivante :

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

Pour en savoir plus sur l'interrogation de documents par emplacement physique, consultez ce guide de solution .

Énumérations

Les énumérations sont probablement l'une des fonctionnalités linguistiques les plus sous-estimées de Swift ; ils ont bien plus à offrir qu'il n'y paraît. Un cas d'utilisation courant des énumérations consiste à modéliser les états discrets de quelque chose. Par exemple, nous pourrions écrire une application pour gérer des articles. Pour suivre le statut d'un article, nous pourrions vouloir utiliser une énumération Status :

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

Cloud Firestore ne prend pas en charge les énumérations de manière native (c'est-à-dire qu'il ne peut pas appliquer l'ensemble de valeurs), mais nous pouvons toujours profiter du fait que les énumérations peuvent être saisies et choisir un type codable. Dans cet exemple, nous avons choisi String , ce qui signifie que toutes les valeurs d'énumération seront mappées vers/depuis une chaîne lorsqu'elles seront stockées dans un document Cloud Firestore.

Et comme Swift prend en charge les valeurs brutes personnalisées, nous pouvons même personnaliser les valeurs qui font référence à quel cas d'énumération. Ainsi, par exemple, si nous décidons de stocker le cas Status.inReview comme « en cours de révision », nous pourrions simplement mettre à jour l'énumération ci-dessus comme suit :

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

Personnalisation du mappage

Parfois, les noms d'attributs des documents Cloud Firestore que nous souhaitons mapper ne correspondent pas aux noms des propriétés de notre modèle de données dans Swift. Par exemple, l'un de nos collègues pourrait être un développeur Python et a décidé de choisir Snake_case pour tous ses noms d'attributs.

Ne vous inquiétez pas : Codable nous couvre !

Pour des cas comme ceux-ci, nous pouvons utiliser CodingKeys . Il s'agit d'une énumération que nous pouvons ajouter à une structure codable pour spécifier comment certains attributs seront mappés.

Considérez ce document :

Un document Firestore avec un nom d'attribut Snake_cased

Pour mapper ce document à une structure qui a une propriété name de type String , nous devons ajouter une énumération CodingKeys à la structure ProgrammingLanguage et spécifier le nom de l'attribut dans le document :

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

Par défaut, l'API Codable utilisera les noms de propriété de nos types Swift pour déterminer les noms d'attribut sur les documents Cloud Firestore que nous essayons de mapper. Ainsi, tant que les noms d'attributs correspondent, il n'est pas nécessaire d'ajouter CodingKeys à nos types codables. Cependant, une fois que nous utilisons CodingKeys pour un type spécifique, nous devons ajouter tous les noms de propriétés que nous souhaitons mapper.

Dans l'extrait de code ci-dessus, nous avons défini une propriété id que nous souhaiterions peut-être utiliser comme identifiant dans une vue List SwiftUI. Si nous ne le spécifiions pas dans CodingKeys , il ne serait pas mappé lors de la récupération des données et deviendrait ainsi nil . Cela entraînerait le remplissage de la vue List avec le premier document.

Toute propriété qui n’est pas répertoriée comme cas dans l’énumération CodingKeys respective sera ignorée pendant le processus de mappage. Cela peut en fait être pratique si nous souhaitons spécifiquement exclure certaines propriétés du mappage.

Ainsi, par exemple, si nous voulons exclure la propriété reasonWhyILoveThis du mappage, tout ce que nous avons à faire est de la supprimer de l'énumération 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
  }
}

Parfois, nous souhaitons peut-être réécrire un attribut vide dans le document Cloud Firestore. Swift a la notion d'options pour indiquer l'absence de valeur, et Cloud Firestore prend également en charge les valeurs null . Cependant, le comportement par défaut pour le codage des éléments facultatifs ayant une valeur nil consiste simplement à les omettre. @ExplicitNull nous donne un certain contrôle sur la manière dont les options Swift sont gérées lors de leur encodage : en signalant une propriété facultative comme @ExplicitNull , nous pouvons demander à Cloud Firestore d'écrire cette propriété dans le document avec une valeur nulle si elle contient une valeur nil .

Utilisation d'un encodeur et d'un décodeur personnalisés pour mapper les couleurs

Comme dernier sujet de notre couverture des données cartographiques avec Codable, introduisons les encodeurs et décodeurs personnalisés. Cette section ne couvre pas un type de données Cloud Firestore natif, mais les encodeurs et décodeurs personnalisés sont très utiles dans vos applications Cloud Firestore.

"Comment puis-je mapper les couleurs" est l'une des questions les plus fréquemment posées par les développeurs, non seulement pour Cloud Firestore, mais également pour le mappage entre Swift et JSON. Il existe de nombreuses solutions, mais la plupart d'entre elles se concentrent sur JSON et presque toutes mappent les couleurs sous la forme d'un dictionnaire imbriqué composé de ses composants RVB.

Il semble qu'il devrait y avoir une solution meilleure et plus simple. Pourquoi n'utilisons-nous pas des couleurs Web (ou, pour être plus précis, la notation de couleur hexadécimale CSS) : elles sont faciles à utiliser (essentiellement juste une chaîne) et elles prennent même en charge la transparence !

Pour pouvoir mapper une Swift Color à sa valeur hexadécimale, nous devons créer une extension Swift qui ajoute Codable à Color .

extension Color {

 init(hex: String) {
    let rgba = hex.toRGBA()

    self.init(.sRGB,
              red: Double(rgba.r),
              green: Double(rgba.g),
              blue: Double(rgba.b),
              opacity: Double(rgba.alpha))
    }

    //... (code for translating between hex and RGBA omitted for brevity)

}

extension Color: Codable {
  
  public init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let hex = try container.decode(String.self)

    self.init(hex: hex)
  }
  
  public func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    try container.encode(toHex)
  }

}

En utilisant decoder.singleValueContainer() , nous pouvons décoder une String en son équivalent Color , sans avoir à imbriquer les composants RGBA. De plus, vous pouvez utiliser ces valeurs dans l'interface utilisateur Web de votre application, sans avoir à les convertir au préalable !

Grâce à cela, nous pouvons mettre à jour le code de mappage des balises, ce qui facilite la gestion directe des couleurs des balises au lieu d'avoir à les mapper manuellement dans le code de l'interface utilisateur de notre application :

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

Gestion des erreurs

Dans les extraits de code ci-dessus, nous avons intentionnellement réduit la gestion des erreurs au minimum, mais dans une application de production, vous devez vous assurer de gérer correctement toutes les erreurs.

Voici un extrait de code qui montre comment gérer toutes les situations d'erreur que vous pourriez rencontrer :

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

Gestion des erreurs dans les mises à jour en direct

L'extrait de code précédent montre comment gérer les erreurs lors de la récupération d'un seul document. En plus de récupérer les données une seule fois, Cloud Firestore prend également en charge la fourniture de mises à jour de votre application au fur et à mesure qu'elles se produisent, à l'aide de ce que l'on appelle des écouteurs d'instantanés : nous pouvons enregistrer un écouteur d'instantané sur une collection (ou une requête), et Cloud Firestore appellera notre écouteur à chaque fois. est une mise à jour.

Voici un extrait de code qui montre comment enregistrer un écouteur d'instantané, mapper les données à l'aide de Codable et gérer les erreurs qui pourraient survenir. Il montre également comment ajouter un nouveau document à la collection. Comme vous le verrez, il n'est pas nécessaire de mettre à jour nous-mêmes le tableau local contenant les documents mappés, car cela est pris en charge par le code dans l'écouteur d'instantanés.

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

Tous les extraits de code utilisés dans cet article font partie d'un exemple d'application que vous pouvez télécharger à partir de ce référentiel GitHub .

Allez-y et utilisez Codable !

L'API Codable de Swift offre un moyen puissant et flexible de mapper des données à partir de formats sérialisés vers et depuis le modèle de données de vos applications. Dans ce guide, vous avez vu à quel point il est facile à utiliser dans les applications qui utilisent Cloud Firestore comme banque de données.

En partant d'un exemple basique avec des types de données simples, nous avons progressivement augmenté la complexité du modèle de données, tout en pouvant nous appuyer sur l'implémentation de Codable et Firebase pour réaliser le mapping à notre place.

Pour plus de détails sur Codable, je recommande les ressources suivantes :

Bien que nous ayons fait de notre mieux pour compiler un guide complet pour mapper les documents Cloud Firestore, celui-ci n'est pas exhaustif et vous utilisez peut-être d'autres stratégies pour mapper vos types. À l'aide du bouton Envoyer des commentaires ci-dessous, indiquez-nous les stratégies que vous utilisez pour mapper d'autres types de données Cloud Firestore ou représenter des données dans Swift.

Il n'y a vraiment aucune raison de ne pas utiliser le support Codable de Cloud Firestore.