Cloud Firestore iOS Codelab

Mantieni tutto organizzato con le raccolte Salva e classifica i contenuti in base alle tue preferenze.

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

Prerequisiti

Prima di iniziare questo codelab assicurati di aver installato:

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

2. Crea il progetto della console Firebase

Aggiungi Firebase al progetto

  1. Vai alla console di Firebase .
  2. Selezionare Crea nuovo progetto e denominare il 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 arrestarsi 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 che hai il tuo progetto, scarica il file GoogleService-Info.plist del tuo progetto dalla console Firebase e trascinalo nella radice del progetto Xcode. Esegui di nuovo il progetto per assicurarti che l'app sia configurata correttamente e non si blocchi più all'avvio. Dopo aver effettuato l'accesso, dovresti vedere una schermata vuota come nell'esempio qui sotto. 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 dati su Firestore

In questa sezione scriveremo alcuni dati in 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 di 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 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. Aggiungere 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 precedente 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 sull'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 involontarie.

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

Discuteremo le regole di sicurezza in dettaglio 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.

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

Schermata 2017-07-06 at 12.45.38 PM.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 listener riceverà una notifica di tutti i dati esistenti che corrispondono alla query e riceverà aggiornamenti in tempo reale.

Per prima cosa, 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 collegare uno snapshot listener per caricare i dati da Firestore nella nostra app. Aggiungere 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 addSnapshotListener(_:) aggiunge uno snapshot listener 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 che questo snapshot listener 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 alle strutture (vedi Restaurant.swift ), la visualizzazione dei dati è solo una questione di assegnazione di alcune proprietà di visualizzazione. Aggiungere le righe seguenti 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 populate 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 di nuovo 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 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 "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 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 i nomi di categoria e 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/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. L'apertura del collegamento dal messaggio di errore aprirà automaticamente l'interfaccia utente di creazione dell'indice nella console Firebase con i parametri corretti inseriti. Per ulteriori informazioni sugli indici in Firestore, visita la documentazione .

7. Scrivere 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 loro ha commesso un errore, probabilmente chiederemo semplicemente all'utente di riprovarli o riprovarli 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 altro.

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

Aggiungere 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 effettuiamo utilizzando l'oggetto transazione verranno trattate come un singolo aggiornamento atomico da Firestore. Se l'aggiornamento non riesce sul server, 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 in cui sta tentando di scrivere.

8. Regole di sicurezza

Gli utenti della nostra app non dovrebbero essere in grado di leggere e scrivere tutti i dati 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 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 di cui 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 alla sottoraccolta denominata ratings di qualsiasi documento appartenente alla raccolta restaurants . La condizione di allow write impedisce quindi l'invio di qualsiasi recensione se l'ID utente della recensione non corrisponde a quello dell'utente. La seconda dichiarazione di corrispondenza consente a qualsiasi utente autenticato di leggere e scrivere ristoranti nel database.

Funziona davvero 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: 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 anche essere utilizzate in modo più granulare per limitare le scritture sui 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 modifichi 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 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 che un ristorante è stato scritto, il suo nome, posizione, prezzo e categoria non possono essere modificati. In particolare, l'ultima regola richiede 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 ulteriori informazioni su Firestore, visita le seguenti risorse: