Atelier de programmation iOS Cloud Firestore

Restez organisé à l'aide des collections Enregistrez et classez les contenus selon vos préférences.

1. Vue d'ensemble

Buts

Dans cet atelier de programmation, vous allez créer une application de recommandation de restaurants 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 apportées aux données Firestore en temps réel
  3. Utiliser l'authentification Firebase et les règles de sécurité pour sécuriser les données Firestore
  4. Écrire des requêtes Firestore complexes

Conditions préalables

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

  • Xcode version 13.0 (ou supérieure)
  • CocoaPods 1.11.0 (ou supérieur)

2. Créer un projet de console Firebase

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".

3. Obtenez l'exemple de projet

Télécharger le code

Commencez par cloner l' exemple de projet et exécutez la mise à 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.

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 de votre projet depuis la console Firebase et faites-le glisser à la racine du projet Xcode. Exécutez à nouveau le projet pour vous assurer que l'application se configure correctement et ne se bloque 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.

d5225270159c040b.png

4. Écrire des données sur Firestore

Dans cette section, nous allons écrire des données sur 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.

L'objet modèle principal de notre application est un restaurant. Les données Firestore sont divisées en documents, collections et sous-collections. Nous stockerons chaque restaurant en tant que document dans une collection de niveau supérieur appelée restaurants . Si vous souhaitez en savoir plus sur le modèle de données Firestore, lisez les informations sur 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 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 l'intégralité 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 de suffisamment détendu pour ne pas rencontrer constamment des problèmes d'authentification lors de l'expérimentation. À la fin de cet atelier de programmation, nous verrons comment renforcer vos règles de sécurité et 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, bien que vous ne le voyiez pas encore dans l'application.

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

Capture d'écran 2017-07-06 à 12.45.38 PM.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.

5. Afficher les données de Firestore

Dans cette section, vous apprendrez à récupérer des 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 écouteur sera informé de toutes les données existantes correspondant à 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 non filtrée par défaut. 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 depuis Firestore et la stocke localement dans un tableau. 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 les 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'une modification 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 consiste simplement à attribuer 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 table, qui s'occupe de mapper la collection de types de valeur d'avant aux cellules individuelles de la vue table.

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 !

391c0259bf05ac25.png

6. Trier et filtrer les données

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, seuls les restaurants dont category est "Dim Sum" seront téléchargés.

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 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 renverra 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 permet à Firestore de rester 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 .

7. Écrire des données dans une transaction

Dans cette section, nous ajouterons la possibilité pour les 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 comportait une erreur, nous demanderions probablement à l'utilisateur de les réessayer ou de les réessayer automatiquement.

Afin d'ajouter une note à un restaurant, nous devons coordonner plusieurs lectures et écritures. L'avis lui-même doit d'abord être soumis, puis le nombre d'évaluations et l'évaluation moyenne du restaurant 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 ainsi 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)
    }
  }
}

À 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 l'appareil est complètement hors ligne ou si l'utilisateur n'est pas autorisé à écrire dans le chemin sur lequel il essaie d'écrire.

8. Règles de sécurité

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 notes d'un restaurant, mais seul un utilisateur authentifié devrait être autorisé à publier une note. Il ne suffit pas d'écrire un 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 apprendrons à utiliser les règles de sécurité Firebase pour protéger nos données.

Tout d'abord, examinons plus en détail les règles de sécurité que nous avons écrites au début de l'atelier de programmation. 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 la condition que nous avons ajoutée 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 d'avis afin que l'ID utilisateur de l'avis corresponde à l'ID de l'utilisateur authentifié. Cela garantit que les utilisateurs ne peuvent pas se faire passer pour d'autres et laisser des avis frauduleux. Remplacez vos règles de sécurité par les éléments suivants :

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 toute révision d'être soumise si l'ID utilisateur de la révision ne correspond pas à celui de l'utilisateur. La deuxième déclaration 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 précédemment dans notre application - 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 d'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éer et mettre à jour afin que nous puissions être plus précis sur les opérations qui doivent être autorisées. N'importe quel utilisateur peut écrire des restaurants dans la base de données, en préservant la fonctionnalité du bouton Populate que nous avons créé au début du laboratoire de programmation, mais 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 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 .

9.Conclusion

Dans cet atelier de programmation, vous avez appris à effectuer des lectures et é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 :