Atelier de programmation Cloud Firestore pour iOS

Atelier de programmation iOS Cloud Firestore

À propos de cet atelier de programmation

subjectDernière mise à jour : sept. 15, 2025
account_circleRédigé par une Googleuse ou un Googleur

1. Présentation

Objectifs

Dans cet atelier de programmation, vous allez créer une application de recommandation de restaurants basée sur Firestore sur iOS en Swift. Vous allez apprendre à effectuer les tâches suivantes :

  1. Lire et écrire des données dans Firestore à partir d'une application iOS
  2. Écouter les modifications apportées aux données Firestore en temps réel
  3. Utiliser Firebase Authentication et les règles de sécurité pour sécuriser des données Firestore
  4. Écrire des requêtes Firestore complexes

Prérequis

Avant de commencer cet atelier de programmation, assurez-vous d'avoir installé les éléments suivants :

  • Xcode version 14.0 (ou version ultérieure)
  • CocoaPods 1.12.0 (ou version ultérieure)

2. Obtenir l'exemple de projet

Télécharger le code

Commencez par cloner l'exemple de projet et exécutez pod update dans le répertoire du projet :

git clone https://github.com/firebase/friendlyeats-ios
cd friendlyeats-ios
pod update

Ouvrez FriendlyEats.xcworkspace dans Xcode et exécutez-le (Cmd+R). L'application doit se compiler correctement et planter immédiatement au lancement, car il manque un fichier GoogleService-Info.plist. Nous corrigerons cela à l'étape suivante.

3. Configurer Firebase

Créer un projet Firebase

  1. Connectez-vous à la console Firebase à l'aide de votre compte Google.
  2. Cliquez sur le bouton pour créer un projet, puis saisissez un nom de projet (par exemple, FriendlyEats).
  3. Cliquez sur Continuer.
  4. Si vous y êtes invité, lisez et acceptez les Conditions d'utilisation de Firebase, puis cliquez sur Continuer.
  5. (Facultatif) Activez l'assistance IA dans la console Firebase (appelée "Gemini dans Firebase").
  6. Pour cet atelier de programmation, vous n'avez pas besoin de Google Analytics. Désactivez donc l'option Google Analytics.
  7. Cliquez sur Créer un projet, attendez que votre projet soit provisionné, puis cliquez sur Continuer.

Associer votre application à Firebase

Créez une application iOS dans votre nouveau projet Firebase.

Téléchargez le fichier GoogleService-Info.plist de votre projet depuis la console Firebase, puis faites-le glisser à la racine du projet Xcode. Exécutez à nouveau le projet pour vous assurer que l'application se configure correctement et ne plante plus au lancement. Une fois connecté, un écran vide semblable à l'exemple ci-dessous devrait s'afficher. Si vous ne parvenez pas à vous connecter, assurez-vous d'avoir activé la méthode de connexion par e-mail/mot de passe dans la console Firebase, sous "Authentification".

d5225270159c040b.png

4. Écrire des données dans Firestore

Dans cette section, nous allons écrire des données dans Firestore pour pouvoir remplir l'UI de l'application. Il est possible d'effectuer cette opération manuellement à l'aide de la console Firebase, mais nous le ferons dans l'application pour voir comment se déroule un processus basique d'écriture Firestore.

Le principal objet de modèle dans notre application est un restaurant. Les données Firestore sont divisées en documents, collections et sous-collections. Nous allons stocker chaque restaurant sous forme de document dans une collection de premier niveau appelée restaurants. Pour en savoir plus sur le modèle de données de Firestore, consultez la documentation sur les documents et les collections.

Avant de pouvoir ajouter des données à Firestore, nous devons obtenir une référence à la collection de restaurants. Ajoutez ce qui suit à la boucle "for" interne de la méthode RestaurantsTableViewController.didTapPopulateButton(_:).

let collection = Firestore.firestore().collection("restaurants")

Maintenant que nous avons une référence de collection, nous pouvons écrire des données. Ajoutez le code suivant juste après la dernière ligne de code que nous avons ajoutée :

let collection = Firestore.firestore().collection("restaurants")

// ====== ADD THIS ======
let restaurant
= Restaurant(
  name
: name,
  category
: category,
  city
: city,
  price
: price,
  ratingCount
: 0,
  averageRating
: 0
)

collection
.addDocument(data: restaurant.dictionary)

Le code ci-dessus ajoute un document à la collection "restaurants". Les données du document proviennent d'un dictionnaire que nous obtenons à partir d'une structure Restaurant.

Nous y sommes presque : avant de pouvoir écrire des documents dans Firestore, nous devons ouvrir les règles de sécurité de Firestore et décrire les parties de notre base de données qui doivent être accessibles en écriture par les utilisateurs. Pour l'instant, nous n'autoriserons que les utilisateurs authentifiés à lire et à écrire dans l'ensemble de la base de données. C'est un peu trop permissif pour une application de production, mais pendant le processus de création de l'application, nous voulons quelque chose d'assez détendu pour ne pas rencontrer constamment de problèmes d'authentification lors de nos tests. À la fin de cet atelier de programmation, nous vous expliquerons comment renforcer vos règles de sécurité et limiter les lectures et écritures involontaires.

Dans l'onglet Règles de la console Firebase, ajoutez les règles suivantes, puis cliquez sur Publier.

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurants/{any}/ratings/{rating} {
      // Users can only write ratings with their user ID
      allow read;
      allow write: if request.auth != null
                   && request.auth.uid == request.resource.data.userId;
    }

    match /restaurants/{any} {
      // Only authenticated users can read or write data
      allow read, write: if request.auth != null;
    }
  }
}

Nous aborderons les règles de sécurité en détail plus tard, mais si vous êtes pressé, consultez la documentation sur les règles de sécurité.

Exécutez l'application et connectez-vous. Appuyez ensuite sur le bouton Populate (Remplir) en haut à gauche, ce qui créera un lot de documents de restaurant, bien que vous ne le verrez pas encore dans l'application.

Ensuite, accédez à l'onglet "Données Firestore" de la console Firebase. De nouvelles entrées devraient maintenant s'afficher dans la collection de restaurants :

Screen Shot 2017-07-06 at 12.45.38 PM.png

Félicitations ! Vous venez d'écrire des données dans Firestore à partir d'une application iOS. Dans la section suivante, vous allez apprendre à récupérer des données dans Firestore pour les afficher dans l'application.

5. Afficher les données de Firestore

Dans cette section, vous allez apprendre à récupérer des données depuis Firestore pour les afficher dans l'application. Les deux étapes clés sont la création d'une requête et l'ajout d'un écouteur d'instantanés. Cet écouteur est informé de toutes les données existantes correspondant à la requête et actualisé en temps réel.

Commençons par créer la requête qui diffusera la liste de restaurants par défaut et non filtrée. Examinons l'implémentation de RestaurantsTableViewController.baseQuery() :

return Firestore.firestore().collection("restaurants").limit(to: 50)

Cette requête récupère jusqu'à 50 restaurants de la collection de premier niveau nommée "restaurants". Maintenant que nous avons une requête, nous devons associer un écouteur d'instantanés pour charger les données de Firestore dans notre application. Ajoutez le code suivant à la méthode RestaurantsTableViewController.observeQuery() juste après l'appel de stopObserving().

listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
  guard let snapshot = snapshot else {
    print("Error fetching snapshot results: \(error!)")
    return
  }
  let models = snapshot.documents.map { (document) -> Restaurant in
    if let model = Restaurant(dictionary: document.data()) {
      return model
    } else {
      // Don't use fatalError here in a real app.
      fatalError("Unable to initialize type \(Restaurant.self) with dictionary \(document.data())")
    }
  }
  self.restaurants = models
  self.documents = snapshot.documents

  if self.documents.count > 0 {
    self.tableView.backgroundView = nil
  } else {
    self.tableView.backgroundView = self.backgroundView
  }

  self.tableView.reloadData()
}

Le code ci-dessus télécharge la collection depuis Firestore et la stocke localement dans un tableau. L'appel addSnapshotListener(_:) ajoute un écouteur d'instantanés à la requête, qui met à jour le contrôleur de vue chaque fois que les données sont modifiées sur le serveur. Nous recevons les mises à jour automatiquement et n'avons pas besoin d'appliquer les modifications manuellement. N'oubliez pas que ce listener d'instantané peut être appelé à tout moment à la suite d'une modification côté serveur. Il est donc important que notre application puisse gérer les modifications.

Après avoir mappé nos dictionnaires à des structs (voir Restaurant.swift), l'affichage des données se résume à l'attribution de quelques propriétés de vue. Ajoutez les lignes suivantes à RestaurantTableViewCell.populate(restaurant:) dans RestaurantsTableViewController.swift.

nameLabel.text = restaurant.name
cityLabel
.text = restaurant.city
categoryLabel
.text = restaurant.category
starsView
.rating = Int(restaurant.averageRating.rounded())
priceLabel
.text = priceString(from: restaurant.price)

Cette méthode de remplissage est appelée à partir de la méthode tableView(_:cellForRowAtIndexPath:) de la source de données de la vue de tableau, qui se charge de mapper la collection de types de valeurs d'avant aux cellules individuelles de la vue de tableau.

Exécutez à nouveau l'application et vérifiez que les restaurants affichés précédemment dans la console sont désormais visibles sur le simulateur ou l'appareil. Si vous n'avez pas fait d'erreur dans cette section, votre application lit et écrit désormais des données avec Cloud Firestore.

391c0259bf05ac25.png

6. Trier et filtrer des données

Actuellement, notre application affiche la liste des restaurants, mais l'utilisateur ne peut pas la filtrer en fonction de ses besoins. Dans cette section, vous allez utiliser les fonctionnalités avancées de requête de Firestore pour activer le filtrage.

Voici un exemple de requête basique permettant de récupérer tous les restaurants de dim sum :

let filteredQuery = query.whereField("category", isEqualTo: "Dim Sum")

Comme son nom l'indique, la méthode whereField(_:isEqualTo:) limitera les téléchargements déclenchés par la requête aux membres de la collection dont les champs correspondent aux restrictions que nous avons définies. Dans ce cas, il ne télécharge que les restaurants dont l'attribut category est "Dim Sum".

Dans cette application, l'utilisateur peut combiner plusieurs filtres pour créer des requêtes spécifiques, telles que "Pizza à San Francisco" ou "Fruits de mer à Los Angeles par ordre de popularité".

Ouvrez RestaurantsTableViewController.swift et ajoutez le bloc de code suivant au milieu de query(withCategory:city:price:sortBy:) :

if let category = category, !category.isEmpty {
  filtered
= filtered.whereField("category", isEqualTo: category)
}

if let city = city, !city.isEmpty {
  filtered
= filtered.whereField("city", isEqualTo: city)
}

if let price = price {
  filtered
= filtered.whereField("price", isEqualTo: price)
}

if let sortBy = sortBy, !sortBy.isEmpty {
  filtered
= filtered.order(by: sortBy)
}

L'extrait de code ci-dessus ajoute plusieurs clauses whereField et order afin de générer une requête complexe unique en fonction des entrées utilisateur. Désormais, notre requête ne renverra que les restaurants qui correspondent aux besoins de l'utilisateur.

Exécutez votre projet et vérifiez que vous pouvez filtrer les résultats par prix, par ville et par catégorie (assurez-vous de saisir les noms de catégorie et de ville exactement). Au cours de la phase de test, les erreurs qui s'afficheront dans vos journaux se présenteront ainsi :

Error fetching snapshot results: Error Domain=io.grpc Code=9
"The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_composite=..."
UserInfo={NSLocalizedDescription=The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_composite=...}

En effet, Firestore nécessite une indexation préalable pour la plupart des requêtes complexes. Le fait d'exiger une indexation sur les requêtes permet à Firestore d'être rapide à grande échelle. L'ouverture du lien à partir du message d'erreur entraîne l'ouverture automatique de l'interface utilisateur de création d'index dans la console Firebase avec les paramètres corrects renseignés. Pour en savoir plus sur les index dans Firestore, consultez la documentation.

7. Écrire des données dans une transaction

Dans cette section, nous allons donner aux utilisateurs la possibilité d'envoyer des avis sur des restaurants. Jusqu'à présent, toutes nos écritures étaient indépendantes et relativement simples. En cas d'erreur, il vous suffisait de demander à l'utilisateur de réessayer ou de réessayer automatiquement.

Pour ajouter une note à un restaurant, nous devons coordonner plusieurs opérations de lecture et d'écriture. Tout d'abord, l'avis lui-même doit être envoyé, puis le nombre de notes et la note moyenne du restaurant doivent être actualisés. Si l'une des deux opérations échoue, mais pas l'autre, nous nous retrouvons dans un état incohérent où les données d'une partie de notre base de données ne correspondent pas à celles d'une autre partie.

Heureusement, Firestore propose une fonctionnalité de transaction qui nous permet d'effectuer plusieurs opérations de lecture et d'écriture en une seule opération atomique afin de garantir la cohérence de nos données.

Ajoutez le code suivant sous toutes les déclarations "let" dans RestaurantDetailViewController.reviewController(_:didSubmitFormWithReview:).

let firestore = Firestore.firestore()
firestore
.runTransaction({ (transaction, errorPointer) -> Any? in

 
// Read data from Firestore inside the transaction, so we don't accidentally
 
// update using stale client data. Error if we're unable to read here.
  let restaurantSnapshot
: DocumentSnapshot
 
do {
   
try restaurantSnapshot = transaction.getDocument(reference)
 
} catch let error as NSError {
    errorPointer
?.pointee = error
   
return nil
 
}

 
// Error if the restaurant data in Firestore has somehow changed or is malformed.
  guard let data
= restaurantSnapshot.data(),
        let restaurant
= Restaurant(dictionary: data) else {

    let error
= NSError(domain: "FireEatsErrorDomain", code: 0, userInfo: [
     
NSLocalizedDescriptionKey: "Unable to write to restaurant at Firestore path: \(reference.path)"
   
])
    errorPointer
?.pointee = error
   
return nil
 
}

 
// Update the restaurant's rating and rating count and post the new review at the
 
// same time.
  let newAverage
= (Float(restaurant.ratingCount) * restaurant.averageRating + Float(review.rating))
     
/ Float(restaurant.ratingCount + 1)

  transaction
.setData(review.dictionary, forDocument: newReviewReference)
  transaction
.updateData([
   
"numRatings": restaurant.ratingCount + 1,
   
"avgRating": newAverage
 
], forDocument: reference)
 
return nil
}) { (object, error) in
 
if let error = error {
   
print(error)
 
} else {
   
// Pop the review controller on success
   
if self.navigationController?.topViewController?.isKind(of: NewReviewViewController.self) ?? false {
     
self.navigationController?.popViewController(animated: true)
   
}
 
}
}

Dans le bloc de mise à jour, toutes les opérations que nous effectuons à l'aide de l'objet de transaction seront traitées comme une seule mise à jour atomique par Firestore. Si la mise à jour échoue sur le serveur, Firestore réessaie automatiquement plusieurs fois. Cela signifie que notre condition d'erreur est très probablement une erreur unique qui se produit à plusieurs reprises, par exemple si l'appareil est complètement hors connexion ou si l'utilisateur n'est pas autorisé à écrire dans le chemin dans lequel il essaie d'écrire.

8. Règles de sécurité

Les utilisateurs de notre application ne doivent pas pouvoir lire et écrire toutes les données de notre base de données. Par exemple, tout le monde devrait pouvoir voir les notes d'un restaurant, mais seul un utilisateur authentifié devrait être autorisé à publier une note. Il ne suffit pas d'écrire du bon code sur le client. Nous devons spécifier notre modèle de sécurité des données sur le backend pour être totalement sécurisés. Dans cette section, nous allons apprendre à utiliser les règles de sécurité Firebase pour protéger nos données.

Commençons par examiner de plus près les règles de sécurité que nous avons écrites au début de l'atelier de programmation. Ouvrez la console Firebase, puis accédez à Base de données > Règles dans l'onglet Firestore.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurants/{any}/ratings/{rating} {
      // Users can only write ratings with their user ID
      allow read;
      allow write: if request.auth != null
                   && request.auth.uid == request.resource.data.userId;
    }

    match /restaurants/{any} {
      // Only authenticated users can read or write data
      allow read, write: if request.auth != null;
    }
  }
}

La variable request dans les règles est une variable globale disponible dans toutes les règles. La condition que nous avons ajoutée garantit que la requête est authentifiée avant d'autoriser les utilisateurs à effectuer une action. Cela empêche les utilisateurs non authentifiés d'utiliser l'API Firestore pour apporter des modifications non autorisées à vos données. C'est un bon début, mais nous pouvons faire beaucoup plus avec les règles Firestore.

Nous souhaitons limiter l'écriture d'avis afin que l'ID utilisateur de l'avis corresponde à l'ID de l'utilisateur authentifié. Cela permet de s'assurer que les utilisateurs ne peuvent pas se faire passer les uns pour les autres et laisser des avis frauduleux.

La première instruction de correspondance correspond à la sous-collection nommée ratings de n'importe quel document appartenant à la collection restaurants. La condition allow write empêche ensuite l'envoi d'un avis si l'ID utilisateur de l'avis ne correspond pas à celui de l'utilisateur. La deuxième instruction de correspondance permet à tout utilisateur authentifié de lire et d'écrire des restaurants dans la base de données.

Cela fonctionne très bien pour nos avis, car nous avons utilisé des règles de sécurité pour indiquer explicitement la garantie implicite que nous avons intégrée à notre application plus tôt : les utilisateurs ne peuvent écrire que leurs propres avis. Si nous ajoutions une fonction de modification ou de suppression des avis, ce même ensemble de règles empêcherait également les utilisateurs de modifier ou de supprimer les avis d'autres utilisateurs. Toutefois, les règles Firestore peuvent également être utilisées de manière plus précise pour limiter les écritures sur des champs individuels dans les documents plutôt que sur les documents entiers. Nous pouvons l'utiliser pour permettre aux utilisateurs de modifier uniquement les notes, la note moyenne et le nombre de notes d'un restaurant, ce qui empêche un utilisateur malveillant de modifier le nom ou l'adresse d'un restaurant.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurants/{restaurant} {
      match /ratings/{rating} {
        allow read: if request.auth != null;
        allow write: if request.auth != null
                     && request.auth.uid == request.resource.data.userId;
      }

      allow read: if request.auth != null;
      allow create: if request.auth != null;
      allow update: if request.auth != null
                    && request.resource.data.name == resource.data.name
                    && request.resource.data.city == resource.data.city
                    && request.resource.data.price == resource.data.price
                    && request.resource.data.category == resource.data.category;
    }
  }
}

Ici, nous avons divisé notre autorisation d'écriture en autorisation de création et de mise à jour afin de pouvoir être plus précis sur les opérations autorisées. Tout utilisateur peut écrire des restaurants dans la base de données, en conservant la fonctionnalité du bouton "Populate" que nous avons créé au début de l'atelier de programmation. Toutefois, une fois qu'un restaurant est écrit, son nom, son emplacement, son prix et sa catégorie ne peuvent plus être modifiés. Plus précisément, la dernière règle exige que toute opération de mise à jour d'un restaurant conserve le même nom, la même ville, le même prix et la même catégorie que les champs déjà existants dans la base de données.

Pour en savoir plus sur les règles de sécurité, consultez la documentation.

9. Conclusion

Dans cet atelier de programmation, vous avez appris à effectuer des lectures et des écritures de base et avancées avec Firestore, ainsi qu'à sécuriser l'accès aux données à l'aide de règles de sécurité. Vous trouverez la solution complète dans la branche codelab-complete.

Pour en savoir plus sur Firestore, consultez les ressources suivantes :