Cloud Firestore iOS Codelab

1. Panoramica

Obiettivi

In questo codelab creerai un'app di consigli sui ristoranti supportata da Firestore su iOS in Swift. Imparerai come:

  1. Leggi e scrivi dati su Firestore da un'app iOS
  2. Ascolta le modifiche ai dati di Firestore in tempo reale
  3. Usa l'autenticazione Firebase e le regole di sicurezza per proteggere i dati di Firestore
  4. Scrivi complesse query Firestore

Prerequisiti

Prima di iniziare questo codelab assicurati di aver installato:

  • Xcode versione 13.0 (o successiva)
  • CocoaPods 1.11.0 (o superiore)

2. Crea un progetto per la console Firebase

Aggiungi Firebase al progetto

  1. Vai alla console Firebase .
  2. Seleziona Crea nuovo progetto e chiama il tuo progetto "Firestore iOS Codelab".

3. Ottieni il progetto di esempio

Scarica il codice

Inizia clonando il progetto di esempio ed eseguendo l' pod update nella directory del progetto:

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

Apri FriendlyEats.xcworkspace in Xcode ed eseguilo (Cmd+R). L'app dovrebbe essere compilata correttamente e andare immediatamente in crash all'avvio, poiché manca un file GoogleService-Info.plist . Lo correggeremo nel passaggio successivo.

Configura Firebase

Segui la documentazione per creare un nuovo progetto Firestore. Una volta ottenuto il progetto, scarica il file GoogleService-Info.plist del progetto dalla console Firebase e trascinalo nella radice del progetto Xcode. Esegui di nuovo il progetto per assicurarti che l'app venga configurata correttamente e non si arresti più in modo anomalo all'avvio. Dopo aver effettuato l'accesso, dovresti vedere una schermata vuota come nell'esempio seguente. Se non riesci ad accedere, assicurati di aver abilitato il metodo di accesso tramite e-mail/password nella console Firebase in Autenticazione.

d5225270159c040b.png

4. Scrivi i dati su Firestore

In questa sezione scriveremo alcuni dati su Firestore in modo da poter popolare l'interfaccia utente dell'app. Questo può essere fatto manualmente tramite la console Firebase , ma lo faremo nell'app stessa per dimostrare una scrittura Firestore di base.

L'oggetto modello principale nella nostra app è un ristorante. I dati di Firestore sono suddivisi in documenti, raccolte e sottoraccolte. Conserveremo ogni ristorante come documento in una raccolta di livello superiore denominata restaurants . Se desideri saperne di più sul modello di dati Firestore, leggi i documenti e le raccolte nella documentazione .

Prima di poter aggiungere dati a Firestore, dobbiamo ottenere un riferimento alla raccolta dei ristoranti. Aggiungi quanto segue al ciclo for interno nel metodo RestaurantsTableViewController.didTapPopulateButton(_:) .

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

Ora che abbiamo un riferimento alla raccolta, possiamo scrivere alcuni dati. Aggiungi quanto segue subito dopo l'ultima riga di codice che abbiamo aggiunto:

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)

Il codice sopra aggiunge un nuovo documento alla collezione dei ristoranti. I dati del documento provengono da un dizionario, che otteniamo da una struttura Restaurant.

Ci siamo quasi: prima di poter scrivere documenti su Firestore, dobbiamo aprire le regole di sicurezza di Firestore e descrivere quali parti del nostro database dovrebbero essere scrivibili da quali utenti. Per ora, consentiremo solo agli utenti autenticati di leggere e scrivere nell'intero database. Questo è un po' troppo permissivo per un'app di produzione, ma durante il processo di creazione dell'app vogliamo qualcosa di abbastanza rilassato in modo da non incorrere costantemente in problemi di autenticazione durante la sperimentazione. Alla fine di questo codelab parleremo di come rafforzare le regole di sicurezza e limitare la possibilità di letture e scritture non intenzionali.

Nella scheda Regole della console Firebase aggiungi le seguenti regole e fai clic su Pubblica .

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

Parleremo in dettaglio delle regole di sicurezza più avanti, ma se hai fretta, dai un'occhiata alla documentazione delle regole di sicurezza .

Esegui l'app e accedi. Quindi tocca il pulsante " Popola " in alto a sinistra, che creerà un batch di documenti del ristorante, anche se non lo vedrai ancora nell'app.

Quindi, vai alla scheda Dati Firestore nella console di Firebase. Ora dovresti vedere le nuove voci nella raccolta dei ristoranti:

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

Congratulazioni, hai appena scritto dati su Firestore da un'app iOS! Nella prossima sezione imparerai come recuperare i dati da Firestore e visualizzarli nell'app.

5. Visualizza i dati da Firestore

In questa sezione imparerai come recuperare i dati da Firestore e visualizzarli nell'app. I due passaggi chiave sono la creazione di una query e l'aggiunta di un listener di snapshot. Questo listener riceverà una notifica di tutti i dati esistenti che corrispondono alla query e riceverà aggiornamenti in tempo reale.

Innanzitutto, costruiamo la query che servirà l'elenco di ristoranti predefinito e non filtrato. Dai un'occhiata all'implementazione di RestaurantsTableViewController.baseQuery() :

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

Questa query recupera fino a 50 ristoranti della raccolta di primo livello denominata "ristoranti". Ora che abbiamo una query, dobbiamo allegare un listener di istantanee per caricare i dati da Firestore nella nostra app. Aggiungi il codice seguente al metodo RestaurantsTableViewController.observeQuery() subito dopo la chiamata a 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()
}

Il codice sopra scarica la raccolta da Firestore e la archivia in un array localmente. La addSnapshotListener(_:) aggiunge un listener di istantanee alla query che aggiornerà il controller di visualizzazione ogni volta che i dati cambiano sul server. Riceviamo gli aggiornamenti automaticamente e non dobbiamo inviare manualmente le modifiche. Ricorda, questo listener di istantanee può essere richiamato in qualsiasi momento come risultato di una modifica lato server, quindi è importante che la nostra app sia in grado di gestire le modifiche.

Dopo aver mappato i nostri dizionari sugli struct (vedi Restaurant.swift ), la visualizzazione dei dati è solo questione di assegnare alcune proprietà di visualizzazione. Aggiungi le seguenti righe a RestaurantTableViewCell.populate(restaurant:) in 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)

Questo metodo di popolamento viene chiamato dal metodo tableView(_:cellForRowAtIndexPath:) dell'origine dati della visualizzazione tabella, che si occupa di mappare la raccolta di tipi di valore da prima alle singole celle della visualizzazione tabella.

Esegui nuovamente l'app e verifica che i ristoranti che abbiamo visto in precedenza nella console siano ora visibili sul simulatore o sul dispositivo. Se hai completato con successo questa sezione, ora la tua app sta leggendo e scrivendo dati con Cloud Firestore!

391c0259bf05ac25.png

6. Ordinamento e filtraggio dei dati

Attualmente la nostra app mostra un elenco di ristoranti, ma non c'è modo per l'utente di filtrare in base alle proprie esigenze. In questa sezione utilizzerai le query avanzate di Firestore per abilitare il filtraggio.

Ecco un esempio di una semplice query per recuperare tutti i ristoranti Dim Sum:

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

Come suggerisce il nome, il whereField(_:isEqualTo:) farà in modo che la nostra query scarichi solo i membri della raccolta i cui campi soddisfano le restrizioni che abbiamo impostato. In questo caso, scaricherà solo i ristoranti la cui category è "Dim Sum" .

In questa app l'utente può concatenare più filtri per creare query specifiche, come "Pizza a San Francisco" o "Frutti di mare a Los Angeles ordinati per popolarità".

Apri RestaurantsTableViewController.swift e aggiungi il seguente blocco di codice al centro della 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)
}

Lo snippet sopra aggiunge più clausole whereField e order per creare una singola query composta basata sull'input dell'utente. Ora la nostra query restituirà solo i ristoranti che soddisfano i requisiti dell'utente.

Esegui il tuo progetto e verifica di poter filtrare per prezzo, città e categoria (assicurati di digitare esattamente la categoria e i nomi delle città). Durante il test potresti vedere errori nei tuoi log che assomigliano a questo:

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

Questo perché Firestore richiede indici per la maggior parte delle query composte. La richiesta di indici sulle query mantiene Firestore veloce su larga scala. L'apertura del collegamento dal messaggio di errore aprirà automaticamente l'interfaccia utente di creazione dell'indice nella console Firebase con i parametri corretti compilati. Per ulteriori informazioni sugli indici in Firestore, visita la documentazione .

7. Scrittura di dati in una transazione

In questa sezione, aggiungeremo la possibilità per gli utenti di inviare recensioni ai ristoranti. Finora, tutte le nostre scritture sono state atomiche e relativamente semplici. Se qualcuno di essi ha commesso un errore, probabilmente chiederemmo all'utente di riprovare o riprovare automaticamente.

Per aggiungere una valutazione a un ristorante, dobbiamo coordinare più letture e scritture. Per prima cosa deve essere inviata la recensione stessa, quindi è necessario aggiornare il conteggio delle valutazioni del ristorante e la valutazione media. Se uno di questi fallisce ma non l'altro, rimaniamo in uno stato incoerente in cui i dati in una parte del nostro database non corrispondono ai dati in un'altra.

Fortunatamente, Firestore offre funzionalità di transazione che ci consentono di eseguire più letture e scritture in un'unica operazione atomica, assicurando che i nostri dati rimangano coerenti.

Aggiungi il codice seguente sotto tutte le dichiarazioni let in 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)
    }
  }
}

All'interno del blocco di aggiornamento, tutte le operazioni che eseguiamo utilizzando l'oggetto transazione verranno trattate come un singolo aggiornamento atomico da Firestore. Se l'aggiornamento non riesce sul server, Firestore lo riproverà automaticamente alcune volte. Ciò significa che la nostra condizione di errore è molto probabilmente un singolo errore che si verifica ripetutamente, ad esempio se il dispositivo è completamente offline o se l'utente non è autorizzato a scrivere nel percorso in cui sta tentando di scrivere.

8. Regole di sicurezza

Gli utenti della nostra app non dovrebbero essere in grado di leggere e scrivere ogni dato nel nostro database. Ad esempio, tutti dovrebbero essere in grado di vedere le valutazioni di un ristorante, ma solo un utente autenticato dovrebbe essere autorizzato a pubblicare una valutazione. Non è sufficiente scrivere un buon codice sul client, dobbiamo specificare il nostro modello di sicurezza dei dati sul back-end per essere completamente sicuri. In questa sezione impareremo come utilizzare le regole di sicurezza di Firebase per proteggere i nostri dati.

Per prima cosa, diamo uno sguardo più approfondito alle regole di sicurezza che abbiamo scritto all'inizio del codelab. Apri la console Firebase e vai a Database > Regole nella scheda 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 variabile di request nelle regole sopra è una variabile globale disponibile in tutte le regole e il condizionale che abbiamo aggiunto garantisce che la richiesta sia autenticata prima di consentire agli utenti di fare qualsiasi cosa. Ciò impedisce agli utenti non autenticati di utilizzare l'API Firestore per apportare modifiche non autorizzate ai tuoi dati. Questo è un buon inizio, ma possiamo usare le regole di Firestore per fare cose molto più potenti.

Limitiamo le scritture delle recensioni in modo che l'ID utente della recensione corrisponda all'ID dell'utente autenticato. Ciò garantisce che gli utenti non possano impersonarsi a vicenda e lasciare recensioni fraudolente. Sostituisci le tue regole di sicurezza con le seguenti:

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 prima dichiarazione di corrispondenza corrisponde alle ratings con nome della sottoraccolta di qualsiasi documento appartenente alla raccolta dei restaurants . Il condizionale allow write impedisce quindi l'invio di qualsiasi recensione se l'ID utente della recensione non corrisponde a quello dell'utente. La seconda istruzione di corrispondenza consente a qualsiasi utente autenticato di leggere e scrivere ristoranti nel database.

Funziona molto bene per le nostre recensioni, poiché abbiamo utilizzato le regole di sicurezza per affermare esplicitamente la garanzia implicita che abbiamo scritto in precedenza nella nostra app, ovvero che gli utenti possono scrivere solo le proprie recensioni. Se dovessimo aggiungere una funzione di modifica o eliminazione per le recensioni, questo identico insieme di regole impedirebbe anche agli utenti di modificare o eliminare anche le recensioni di altri utenti. Ma le regole di Firestore possono essere utilizzate anche in modo più dettagliato per limitare le scritture su singoli campi all'interno dei documenti piuttosto che sull'intero documento stesso. Possiamo usarlo per consentire agli utenti di aggiornare solo le valutazioni, la valutazione media e il numero di valutazioni per un ristorante, eliminando la possibilità che un utente malintenzionato alteri il nome o la posizione di un ristorante.

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

Qui abbiamo suddiviso la nostra autorizzazione di scrittura in creazione e aggiornamento in modo da poter essere più specifici su quali operazioni dovrebbero essere consentite. Qualsiasi utente può scrivere ristoranti nel database, preservando la funzionalità del pulsante Popola che abbiamo creato all'inizio del codelab, ma una volta che un ristorante è stato scritto il suo nome, posizione, prezzo e categoria non possono essere modificati. In particolare, l'ultima regola prevede che qualsiasi operazione di aggiornamento del ristorante mantenga lo stesso nome, città, prezzo e categoria dei campi già esistenti nel database.

Per saperne di più su cosa puoi fare con le regole di sicurezza, dai un'occhiata alla documentazione .

9. Conclusione

In questo codelab, hai imparato come eseguire letture e scritture di base e avanzate con Firestore, nonché come proteggere l'accesso ai dati con regole di sicurezza. Puoi trovare la soluzione completa sul ramo codelab-complete .

Per saperne di più su Firestore, visita le seguenti risorse: