Check out what’s new from Firebase@ Google I/O 2021, and join our alpha program for early access to the new Remote Config personalization feature. Learn more

Laboratoire de codes Cloud Firestore iOS

Buts

Dans ce codelab, vous allez créer une application de recommandation de restaurant basée sur Firestore sur iOS dans Swift. Vous apprendrez à:

  1. Lire et écrire des données sur Firestore à partir d'une application iOS
  2. Écoutez les modifications des données Firestore en temps réel
  3. Utilisez l'authentification Firebase et les règles de sécurité pour sécuriser les données Firestore
  4. Rédiger des requêtes Firestore complexes

Conditions préalables

Avant de démarrer ce codelab, assurez-vous d'avoir installé:

  • Xcode version 8.3 (ou supérieure)
  • CocoaPods 1.2.1 (ou supérieur)

Ajouter Firebase au projet

  1. Accédez à la console Firebase .
  2. Sélectionnez Créer un nouveau projet et nommez votre projet "Firestore iOS Codelab".

Téléchargez le code

Commencez par cloner l' exemple de projet et exécuter la pod update à 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 se bloquer immédiatement au lancement, car il manque un fichier GoogleService-Info.plist . Nous corrigerons cela à l'étape suivante.

Configurer Firebase

Suivez la documentation pour créer un nouveau projet Firestore. Une fois que vous avez votre projet, téléchargez le fichier GoogleService-Info.plist votre projet à partir de la console Firebase et faites-le glisser vers la racine du projet Xcode. Exécutez à nouveau le projet pour vous assurer que l'application se configure correctement et qu'elle ne plante plus au lancement. Après vous être connecté, vous devriez voir un écran vide comme dans l'exemple ci-dessous. 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.

10a0671ce8f99704.png

Dans cette section, nous allons écrire des données dans Firestore afin de pouvoir remplir l'interface utilisateur de l'application. Cela peut être fait manuellement via la console Firebase , mais nous le ferons dans l'application elle-même pour démontrer une écriture Firestore de base.

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

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 dans 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 ce qui suit 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 nouveau document à la collection des restaurants. Les données du document proviennent d'un dictionnaire, que nous obtenons d'une structure Restaurant.

Nous y sommes presque - avant de pouvoir écrire des documents sur Firestore, nous devons ouvrir les règles de sécurité de Firestore et décrire quelles parties de notre base de données doivent être accessibles en écriture par quels utilisateurs. Pour l'instant, nous n'autoriserons que les utilisateurs authentifiés à lire et écrire dans toute 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 de suffisamment détendu pour ne pas constamment rencontrer de problèmes d'authentification lors de l'expérimentation. À la fin de ce codelab, nous parlerons de la manière de renforcer vos règles de sécurité et de limiter la possibilité de lectures et d'é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 /{document=**} {
      //
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      //
      allow read, write: if request.auth != null;
    }
  }
}

Nous discuterons des règles de sécurité en détail plus tard, mais si vous êtes pressé, jetez un œil à la documentation des règles de sécurité .

Exécutez l'application et connectez-vous. Appuyez ensuite sur le bouton " Remplir " en haut à gauche, ce qui créera un lot de documents de restaurant, même si vous ne le verrez pas encore dans l'application.

Ensuite, accédez à l' onglet de données Firestore dans la console Firebase. Vous devriez maintenant voir de nouvelles entrées dans la collection de restaurants:

Capture d'écran 06/07/2017 à 12h45h38.png

Félicitations, vous venez d'écrire des données sur Firestore à partir d'une application iOS! Dans la section suivante, vous apprendrez à récupérer des données de Firestore et à les afficher dans l'application.

Dans cette section, vous apprendrez à récupérer les données de Firestore et à 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é. Cet auditeur sera informé de toutes les données existantes qui correspondent à la requête et recevra des mises à jour en temps réel.

Tout d'abord, construisons la requête qui servira la liste de restaurants par défaut, non filtrée. Jetez un œil à 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 niveau supérieur nommée "restaurants". Maintenant que nous avons une requête, nous devons attacher un écouteur d'instantané pour charger les données de Firestore dans notre application. Ajoutez le code suivant à la méthode RestaurantsTableViewController.observeQuery() juste après l'appel à 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 à partir de Firestore et la stocke dans un tableau localement. L' addSnapshotListener(_:) ajoute un écouteur d'instantané à la requête qui mettra à jour le contrôleur de vue à chaque fois que les données changent sur le serveur. Nous obtenons des mises à jour automatiquement et n'avons pas à pousser manuellement les modifications. N'oubliez pas que cet écouteur d'instantané peut être appelé à tout moment à la suite d'un changement côté serveur, il est donc important que notre application puisse gérer les modifications.

Après avoir mappé nos dictionnaires sur des structures (voir Restaurant.swift ), l'affichage des données est juste une question d'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 tableView(_:cellForRowAtIndexPath:) est appelée à partir de la méthode tableView(_:cellForRowAtIndexPath:) source de données de vue de table, qui prend en charge le mappage de la collection de types de valeur d'avant aux cellules de vue de tableau individuelles.

Exécutez à nouveau l'application et vérifiez que les restaurants que nous avons vus précédemment dans la console sont désormais visibles sur le simulateur ou l'appareil. Si vous avez terminé cette section avec succès, votre application lit et écrit maintenant des données avec Cloud Firestore!

2ca7f8c6052f7f79.png

Actuellement, notre application affiche une liste de restaurants, mais l'utilisateur n'a aucun moyen de filtrer en fonction de ses besoins. Dans cette section, vous utiliserez les requêtes avancées de Firestore pour activer le filtrage.

Voici un exemple de requête simple pour récupérer tous les restaurants Dim Sum:

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

Comme son nom l'indique, la whereField(_:isEqualTo:) fera en sorte que notre requête ne télécharge que les membres de la collection dont les champs respectent les restrictions que nous avons définies. Dans ce cas, il ne téléchargera que les restaurants dont la category est "Dim Sum" .

Dans cette application, l'utilisateur peut enchaîner plusieurs filtres pour créer des requêtes spécifiques, comme "Pizza à San Francisco" ou "Fruits de mer à Los Angeles classés par popularité".

Ouvrez RestaurantsTableViewController.swift et ajoutez le bloc de code suivant au milieu de la 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 whereField ci-dessus ajoute plusieurs clauses whereField et order pour créer une seule requête composée basée sur l'entrée de l'utilisateur. Désormais, notre requête ne renvoie que les restaurants qui correspondent aux besoins de l'utilisateur.

Exécutez votre projet et vérifiez que vous pouvez filtrer par prix, ville et catégorie (assurez-vous de saisir exactement les noms de catégorie et de ville). Lors du test, vous pouvez voir des erreurs dans vos journaux qui ressemblent à ceci:

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/testapp-5d356/database/firestore/indexes?create_index=..." 
UserInfo={NSLocalizedDescription=The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_index=...}

En effet, Firestore nécessite des index pour la plupart des requêtes composées. Exiger des index sur les requêtes maintient Firestore rapide à grande échelle. L'ouverture du lien à partir du message d'erreur ouvrira automatiquement 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 .

Dans cette section, nous ajouterons la possibilité aux utilisateurs de soumettre des avis aux restaurants. Jusqu'à présent, toutes nos écritures ont été atomiques et relativement simples. Si l'un d'entre eux a fait une erreur, nous demanderions probablement simplement à l'utilisateur de le réessayer ou de le réessayer automatiquement.

Afin d'ajouter une note à un restaurant, nous devons coordonner plusieurs lectures et écritures. Tout d'abord, le commentaire lui-même doit être soumis, puis le nombre de notes du restaurant et la note moyenne doivent être mis à jour. Si l'un d'entre eux é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 aux données d'une autre.

Heureusement, Firestore fournit une fonctionnalité de transaction qui nous permet d'effectuer plusieurs lectures et écritures en une seule opération atomique, garantissant que nos données restent cohérentes.

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

À l'intérieur du 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 la réessayera automatiquement plusieurs fois. Cela signifie que notre condition d'erreur est très probablement une erreur unique se produisant à plusieurs reprises, par exemple si le périphérique est complètement hors ligne ou si l'utilisateur n'est pas autorisé à écrire sur le chemin sur lequel il essaie d'écrire.

Les utilisateurs de notre application ne doivent pas être en mesure de lire et d'écrire toutes les données de notre base de données. Par exemple, tout le monde devrait pouvoir voir les évaluations d'un restaurant, mais seul un utilisateur authentifié devrait être autorisé à publier une évaluation. 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 complètement sécurisé. Dans cette section, nous allons apprendre à utiliser les règles de sécurité Firebase pour protéger nos données.

Tout d'abord, examinons de plus près les règles de sécurité que nous avons écrites au début du codelab. Ouvrez la console Firebase et accédez à Base de données> Règles dans l'onglet Firestore .

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      // Only authenticated users can read or write data
      allow read, write: if request.auth != null;
    }
  }
}

La variable de request dans les règles ci-dessus est une variable globale disponible dans toutes les règles, et le conditionnel que nous avons ajouté garantit que la demande est authentifiée avant d'autoriser les utilisateurs à faire quoi que ce soit. 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 utiliser les règles Firestore pour faire des choses beaucoup plus puissantes.

Limitons les écritures de révision afin que l'ID utilisateur de la critique corresponde à l'ID de l'utilisateur authentifié. Cela garantit que les utilisateurs ne peuvent pas se faire passer pour les uns les autres et laisser des avis frauduleux. Remplacez vos règles de sécurité par les suivantes:

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 première déclaration de correspondance correspond à la sous-collection nommée ratings de tout document appartenant à la collection restaurants . La condition allow write empêche alors la soumission de tout 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 énoncer explicitement la garantie implicite que nous avons écrite dans notre application plus tôt - que les utilisateurs ne peuvent rédiger que leurs propres avis. Si nous devions ajouter une fonction de modification ou de suppression pour les avis, ce même ensemble de règles empêcherait également les utilisateurs de modifier ou de supprimer les avis d'autres utilisateurs. Mais les règles Firestore peuvent également être utilisées de manière plus granulaire pour limiter les écritures sur des champs individuels dans les documents plutôt que sur l'ensemble des documents eux-mêmes. Nous pouvons l'utiliser pour permettre aux utilisateurs de mettre à jour uniquement les notes, la note moyenne et le nombre de notes pour un restaurant, supprimant ainsi la possibilité qu'un utilisateur malveillant modifie le nom ou l'emplacement 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 création et mise à jour afin que nous puissions être plus précis sur les opérations à autoriser. Tout utilisateur peut écrire des restaurants dans la base de données, en préservant la fonctionnalité du bouton Remplir que nous avons créé au début du codelab, mais une fois qu'un restaurant est écrit, son nom, son emplacement, son prix et sa catégorie ne peuvent pas être modifiés. Plus précisément, la dernière règle exige que toute opération de mise à jour de 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 ce que vous pouvez faire avec les règles de sécurité, consultez la documentation .

Dans ce laboratoire de codes, 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 avec des règles de sécurité. Vous pouvez trouver la solution complète sur la branche codelab-complete .

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