Codelab iOS di Cloud Firestore

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 Firestore in tempo reale
  3. Utilizza l'autenticazione Firebase e le regole di sicurezza per proteggere i dati Firestore
  4. Scrivi query Firestore complesse

Prerequisiti

Prima di iniziare questo codelab assicurati di aver installato:

  • Xcode versione 14.0 (o successiva)
  • CocoaPods 1.12.0 (o successiva)

2. Crea un progetto della console Firebase

Aggiungi Firebase al progetto

  1. Vai alla console Firebase .
  2. Seleziona Crea nuovo progetto e dai un nome al tuo progetto "Firestore iOS Codelab".

3. Ottieni il progetto di esempio

Scarica il codice

Inizia clonando il progetto di esempio ed eseguendo 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 compilarsi correttamente e bloccarsi immediatamente 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 nuovamente 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 email/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. Questa operazione può essere eseguita 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 Firestore sono suddivisi in documenti, raccolte e sottoraccolte. Memorizzeremo ogni ristorante come documento in una raccolta di primo livello chiamata restaurants . Se desideri saperne di più sul modello 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 raccolta 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 imbatterci 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 involontarie.

Nella scheda Regole della console Firebase aggiungi le seguenti regole e quindi 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;
    }
  }
}

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

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

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

Schermata 2017-07-06 alle 12.45.38.png

Congratulazioni, hai appena scritto dati su Firestore da un'app iOS! Nella sezione successiva 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 ascoltatore 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 predefinito e non filtrato di ristoranti. 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 collegare un listener di snapshot per caricare i dati da Firestore nella nostra app. Aggiungi il seguente codice 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 chiamata addSnapshotListener(_:) aggiunge un listener di snapshot 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 snapshot può essere richiamato in qualsiasi momento come risultato di una modifica lato server, quindi è importante che la nostra app possa gestire le modifiche.

Dopo aver mappato i nostri dizionari sulle strutture (vedi Restaurant.swift ), visualizzare i 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 compilazione viene chiamato dal metodo tableView(_:cellForRowAtIndexPath:) dell'origine dati della vista tabella, che si occupa di mappare la raccolta di tipi di valore da prima alle singole celle della vista 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 correttamente questa sezione, la tua app ora sta leggendo e scrivendo dati con Cloud Firestore!

391c0259bf05ac25.png

6. Ordinamento e filtraggio dei dati

Attualmente la nostra app visualizza un elenco di ristoranti, ma l'utente non ha modo di filtrarli 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 metodo whereField(_:isEqualTo:) farà sì che la nostra query scarichi solo i membri della raccolta i cui campi soddisfano le restrizioni che abbiamo impostato. In questo caso, verranno scaricati 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 "Pesce 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 precedente aggiunge più clausole whereField e order per creare un'unica 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 visualizzare errori nei tuoi log simili a questi:

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

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

7. Scrittura dei 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 presentasse errori, probabilmente chiederemo semplicemente all'utente di riprovarli o riprovarli automaticamente.

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

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

Aggiungi il seguente codice 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 effettuiamo utilizzando l'oggetto transazione verranno trattate come un singolo aggiornamento atomico da Firestore. Se l'aggiornamento sul server fallisce, Firestore 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 l'utente non è autorizzato a scrivere sul percorso su 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 backend per essere completamente sicuri. In questa sezione impareremo come utilizzare le regole di sicurezza Firebase per proteggere i nostri dati.

Innanzitutto, 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 request nelle regole sopra è una variabile globale disponibile in tutte le regole e il condizionale che abbiamo aggiunto garantisce che la richiesta venga 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 utilizzare le regole di Firestore per fare cose molto più potenti.

Limitiamo la scrittura delle recensioni in modo che l'ID utente della recensione corrisponda all'ID dell'utente autenticato. Ciò garantisce che gli utenti non possano impersonificarsi a vicenda e lasciare recensioni fraudolente. Sostituisci le 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 istruzione di corrispondenza corrisponde alla sottoraccolta denominata ratings di qualsiasi documento appartenente alla raccolta restaurants . La condizione allow write impedisce quindi l'invio di qualsiasi recensione se l'ID utente della recensione non corrisponde a quello dell'utente. La seconda istruzione match 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 dichiarare esplicitamente la garanzia implicita che abbiamo scritto in precedenza nella nostra app: ovvero che gli utenti possono scrivere solo le proprie recensioni. Se aggiungessimo 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ù granulare per limitare le scritture sui singoli campi all'interno dei documenti anziché 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 modifichi il nome o la posizione del 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 il nostro permesso 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 scritto un ristorante, il suo nome, posizione, prezzo e categoria non possono essere modificati. Nello specifico, quest'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 nel ramo codelab-complete .

Per ulteriori informazioni su Firestore, visita le seguenti risorse: