Atelier de programmation iOS Cloud Firestore

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 changements dans les 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 8.3 (ou supérieure)
  • CocoaPods 1.2.1 (ou supérieur)

2. Créer un projet de console Firebase

Ajouter Firebase au projet

  1. Allez à 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 le clonage du exemple de pod update projet et en cours d' exécution 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 l' exécuter (Cmd + R). L'application devrait compiler correctement et planter immédiatement sur le lancement, car il manque un GoogleService-Info.plist fichier. 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 votre projet de GoogleService-Info.plist fichier de 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 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

4. Écrire des données dans Firestore

Dans cette section, nous allons écrire des données dans Firestore afin de pouvoir remplir l'interface utilisateur de l'application. Cela peut se faire manuellement via la console Firebase , mais nous allons le faire 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 enregistrons chaque restaurant comme un document dans une collection de niveau supérieur appelé les 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. Ajouter ce qui suit à l'intérieur pour la boucle dans le RestaurantsTableViewController.didTapPopulateButton(_:) méthode.

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 de 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 autoriserons uniquement 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 de suffisamment détendu pour ne pas rencontrer constamment des problèmes d'authentification lors des expérimentations. À la fin de cet atelier de programmation, nous expliquerons 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 ajouter 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 allons discuter des règles de sécurité en détail plus tard, mais si vous êtes pressé, jetez un oeil à la règles de sécurité des documents .

Exécutez l'application et la connexion. Ensuite , appuyez sur le bouton « Populate » en haut à gauche, ce qui créera un lot de documents restaurant, bien que vous ne verrez pas cela dans l'application encore.

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

Capture d'écran 06/07/2017 à 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 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 comment 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 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 et non filtrée. Jetez un oeil à la mise en œuvre 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 RestaurantsTableViewController.observeQuery() méthode 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. Le addSnapshotListener(_:) appel ajoute un écouteur d'instantané à la requête qui mettra à jour le contrôleur de vue chaque fois que les modifications de données 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 cartographié nos dictionnaires à struct (voir Restaurant.swift ), l' affichage des données est juste une question d'attribuer quelques propriétés de la vue. Ajoutez les lignes suivantes à RestaurantTableViewCell.populate(restaurant:) à 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 est appelée à partir Peupler la table vue source de données tableView(_:cellForRowAtIndexPath:) méthode, qui prend soin de la cartographie de la collection de types de valeur avant de les cellules individuelles de tableau.

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

6. Tri et filtrage des données

Actuellement, notre application affiche une liste de restaurants, mais il n'y a aucun moyen pour l'utilisateur 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, le whereField(_:isEqualTo:) méthode fera notre téléchargement de requête que les membres de la collection dont les champs répondent aux restrictions que nous fixons. Dans ce cas, il ne fera que télécharger des restaurants où 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 commandés par 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 ci - dessus ajoute plusieurs whereField et order des clauses pour construire une requête unique de composé à base d' une entrée 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). Pendant le 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. Ouverture du lien du message d'erreur s'ouvre automatiquement l'interface utilisateur de création d'index dans la console Firebase avec les paramètres appropriés remplis. 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 est erroné, nous demanderons 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. Tout d'abord, l'avis lui-même doit être soumis, puis le nombre de notes et la note 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 ci - dessous 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 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 sur le chemin sur lequel il essaie d'écrire.

8. Règles de sécurité

Les utilisateurs de notre application ne devraient 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 de l'atelier de programmation. Ouvrez la console et Firebase Accédez à la 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 request variable dans les règles ci - dessus est une variable globale disponible dans toutes les règles, et nous avons ajouté sous condition assure que la demande est authentifié 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.

Restreignons 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 l'un l'autre 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 restaurants collection. Le allow write d' allow write conditionnelle empêche alors toute révision d'être soumis si l'ID utilisateur de l'examen ne correspond pas à celle 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 critiques, 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, à savoir que les utilisateurs ne peuvent rédiger que leurs propres critiques. Si nous devions ajouter 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 des 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 qui doivent être autorisées. 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 laboratoire de programmation, 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 nécessite que toute opération de mise à jour du 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é, jetez un oeil à 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 avec des règles de sécurité. Vous pouvez trouver la solution complète sur la codelab-complete branche .

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