Informazioni su questo codelab
1. Panoramica
Obiettivi
In questo codelab creerai un'app di consigli sui ristoranti basata su Firestore su iOS in Swift. Imparerai come:
- Leggere e scrivere dati in Firestore da un'app per iOS
- Ascolta le modifiche ai dati di Firestore in tempo reale
- Utilizzare Firebase Authentication e le regole di sicurezza per proteggere i dati di Firestore
- Scrivere query Firestore complesse
Prerequisiti
Prima di iniziare questo codelab, assicurati di aver installato:
- Xcode versione 14.0 o successive
- CocoaPods 1.12.0 o versioni successive
2. 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 deve essere compilata correttamente e arrestarsi immediatamente all'avvio, poiché manca un file GoogleService-Info.plist
. Lo correggeremo nel passaggio successivo.
3. Configura Firebase
Crea un progetto Firebase
- Accedi alla console Firebase utilizzando il tuo Account Google.
- Fai clic sul pulsante per creare un nuovo progetto, quindi inserisci un nome per il progetto (ad esempio
FriendlyEats
).
- Fai clic su Continua.
- Se richiesto, leggi e accetta i termini di Firebase, quindi fai clic su Continua.
- (Facoltativo) Attiva l'assistenza AI nella console Firebase (denominata "Gemini in Firebase").
- Per questo codelab non hai bisogno di Google Analytics, quindi disattiva l'opzione Google Analytics.
- Fai clic su Crea progetto, attendi il provisioning del progetto, poi fai clic su Continua.
Connetti la tua app a Firebase
Crea un'app per iOS nel nuovo progetto Firebase.
Scarica il file GoogleService-Info.plist
del progetto dalla console Firebase e trascinalo nella root del progetto Xcode. Esegui di nuovo il progetto per assicurarti che l'app venga configurata correttamente e non si arresti più all'avvio. Dopo aver eseguito l'accesso, dovresti visualizzare una schermata vuota come nell'esempio riportato di seguito. Se non riesci ad accedere, assicurati di aver attivato il metodo di accesso con email/password nella console Firebase in Autenticazione.
4. Scrivere dati in Firestore
In questa sezione scriveremo alcuni dati in Firestore in modo da poter popolare la UI dell'app. Puoi farlo manualmente tramite la Console Firebase, ma lo faremo nell'app stessa per dimostrare una scrittura di base di Firestore.
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 vuoi saperne di più sul modello di dati Firestore, leggi la documentazione su documenti e raccolte.
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 riportato 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 in Firestore, dobbiamo aprire le regole di sicurezza di Firestore e descrivere quali parti del nostro database devono essere scrivibili da quali utenti. Per il momento, consentiremo solo agli utenti autenticati di leggere e scrivere nell'intero database. Questo è un po' troppo permissivo per un'app di produzione, ma durante la procedura di creazione dell'app vogliamo qualcosa di abbastanza rilassato per non riscontrare costantemente 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 poi fai clic su Pubblica.
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; } } }
Parleremo in dettaglio delle regole di sicurezza 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.
Poi vai alla scheda Dati Firestore nella console Firebase. Ora dovresti vedere nuove voci nella raccolta di ristoranti:
Congratulazioni, hai appena scritto dati su Firestore da un'app per iOS. Nella sezione successiva imparerai a recuperare i dati da Firestore e a visualizzarli nell'app.
5. Visualizzare i dati di Firestore
In questa sezione imparerai a recuperare i dati da Firestore e a 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, creiamo la query che mostrerà l'elenco predefinito e non filtrato dei 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 riportato sopra scarica la raccolta da Firestore e la memorizza localmente in un array. 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 eseguire il push delle modifiche manualmente. Ricorda che questo listener di snapshot può essere richiamato in qualsiasi momento a seguito di una modifica lato server, pertanto è importante che la nostra app possa 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. 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 visualizzazione tabellare, che si occupa di mappare la raccolta di tipi di valori precedenti alle singole celle della visualizzazione tabellare.
Esegui di nuovo l'app e verifica che i ristoranti visualizzati in precedenza nella console siano ora visibili sul simulatore o sul dispositivo. Se hai completato correttamente questa sezione, la tua app ora legge e scrive dati con Cloud Firestore.
6. Ordinamento e filtraggio dei dati
Al momento la nostra app mostra un elenco di ristoranti, ma l'utente non può filtrare in base alle proprie esigenze. In questa sezione utilizzerai le query avanzate di Firestore per attivare il filtro.
Ecco un esempio di query semplice per recuperare tutti i ristoranti di dim sum:
let filteredQuery = query.whereField("category", isEqualTo: "Dim Sum")
Come suggerisce il nome, il metodo whereField(_:isEqualTo:)
farà in modo che la nostra query scarichi solo i membri della raccolta i cui campi soddisfano le limitazioni che abbiamo impostato. In questo caso, verranno scaricati solo i ristoranti in cui category
è "Dim Sum"
.
In questa app, l'utente può concatenare più filtri per creare query specifiche, ad esempio "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 di 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 in base all'input dell'utente. Ora la nostra query restituirà solo i ristoranti che corrispondono ai requisiti dell'utente.
Esegui il progetto e verifica di poter filtrare per prezzo, città e categoria (assicurati di digitare esattamente i nomi della categoria e della città). Durante il test, potresti visualizzare errori nei log simili 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. Richiedere indici nelle query consente a Firestore di mantenere la velocità su larga scala. Se apri il link dal messaggio di errore, si aprirà automaticamente la UI di creazione dell'indice nella console Firebase con i parametri corretti compilati. Per scoprire di più 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 si verificasse un errore, probabilmente chiederemmo all'utente di riprovare o riproveremmo automaticamente.
Per aggiungere una valutazione a un ristorante, dobbiamo coordinare più letture e scritture. Innanzitutto, la recensione deve essere inviata, dopodiché è necessario aggiornare il conteggio delle valutazioni e la valutazione media del ristorante. Se una di queste operazioni non va a buon fine, ma l'altra sì, ci troviamo in uno stato incoerente in cui i dati in una parte del nostro database non corrispondono a quelli di un'altra.
Fortunatamente, Firestore fornisce una funzionalità di transazione che ci consente di eseguire più letture e scritture in una singola operazione atomica, garantendo la coerenza dei nostri dati.
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 eseguiamo utilizzando l'oggetto transazione verranno trattate come un singolo aggiornamento atomico da Firestore. Se l'aggiornamento non riesce sul server, Firestore ritenterà 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 devono 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 a utilizzare le regole di sicurezza di Firebase per proteggere i nostri dati.
Innanzitutto, diamo un'occhiata più da vicino alle regole di sicurezza che abbiamo scritto all'inizio del codelab. Apri la console Firebase e vai a Database > Rules nella scheda Firestore.
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 variabile request
nelle regole è una variabile globale disponibile in tutte le regole e la condizione che abbiamo aggiunto garantisce che la richiesta venga autenticata prima di consentire agli utenti di fare qualsiasi cosa. In questo modo, gli utenti non autenticati non possono 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.
Vorremmo limitare la scrittura di recensioni in modo che l'ID utente della recensione corrisponda all'ID dell'utente autenticato. In questo modo, gli utenti non possono impersonare altri utenti e lasciare recensioni fraudolente.
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 di corrispondenza consente a qualsiasi utente autenticato di leggere e scrivere ristoranti nel database.
Questo funziona molto bene per le nostre recensioni, in quanto abbiamo utilizzato le regole di sicurezza per dichiarare esplicitamente la garanzia implicita che abbiamo scritto nella nostra app in precedenza: gli utenti possono scrivere solo le proprie recensioni. Se dovessimo aggiungere una funzione di modifica o eliminazione delle recensioni, questo stesso insieme di regole impedirebbe agli utenti di modificare o eliminare anche le recensioni di altri utenti. Tuttavia, le regole di Firestore possono essere utilizzate anche in modo più granulare per limitare le scritture su singoli campi all'interno dei documenti anziché sui documenti stessi. Possiamo utilizzare queste informazioni 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 l'autorizzazione di scrittura in creazione e aggiornamento per poter specificare meglio quali operazioni devono essere consentite. Qualsiasi utente può scrivere ristoranti nel database, mantenendo la funzionalità del pulsante Compila che abbiamo creato all'inizio del codelab, ma una volta scritto un ristorante, il suo nome, la sua posizione, il suo prezzo e la sua categoria non possono essere modificati. Più nello specifico, 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 scoprire di più su cosa puoi fare con le regole di sicurezza, consulta la documentazione.
9. Conclusione
In questo codelab hai imparato a eseguire letture e scritture di base e avanzate con Firestore, nonché a proteggere l'accesso ai dati con le regole di sicurezza. Puoi trovare la soluzione completa nel ramo codelab-complete
.
Per saperne di più su Firestore, consulta le seguenti risorse: