(Facoltativo) Prototipo e test con Firebase Local Emulator Suite
Prima di parlare di come la tua app legge e scrive su Realtime Database, introduciamo un insieme di strumenti che puoi utilizzare per prototipare e testare la funzionalità Realtime Database: Firebase Local Emulator Suite. Se stai provando diversi modelli di dati, ottimizzando le regole di sicurezza o cercando il modo più conveniente per interagire con il backend, poter lavorare in locale senza implementare servizi live può essere un'ottima idea.
Un emulatore Realtime Database fa parte di Local Emulator Suite, che consente alla tua app di interagire con i contenuti e la configurazione del database emulato, nonché, facoltativamente, con le risorse del progetto emulato (funzioni, altri database e regole di sicurezza).
L'utilizzo dell'emulatore Realtime Database prevede pochi passaggi:
- Aggiungendo una riga di codice alla configurazione di test dell'app per connettersi all'emulatore.
- Dalla radice della directory del progetto locale, esegui
firebase emulators:start
. - Effettuare chiamate dal codice prototipo della tua app utilizzando un SDK della piattaforma Realtime Database come di consueto o utilizzando l'API REST Realtime Database.
È disponibile una procedura dettagliata che coinvolge Realtime Database e Cloud Functions. Dai un'occhiata anche all'introduzione di Local Emulator Suite.
Recuperare un FIRDatabaseReference
Per leggere o scrivere dati dal database, devi disporre di un'istanza di FIRDatabaseReference
:
Swift
var ref: DatabaseReference! ref = Database.database().reference()
Objective-C
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
Scrivi dati
Questo documento illustra le nozioni di base per la lettura e la scrittura dei dati di Firebase.
I dati di Firebase vengono scritti in un riferimento Database
e recuperati collegando un listener asincrono al riferimento. Il listener viene attivato
una volta per lo stato iniziale dei dati e di nuovo ogni volta che i dati cambiano.
Operazioni di scrittura di base
Per le operazioni di scrittura di base, puoi utilizzare setValue
per salvare i dati in un riferimento specificato, sostituendo tutti i dati esistenti nel percorso. Puoi utilizzare questo metodo per:
- Tipi di tessere che corrispondono ai tipi JSON disponibili come segue:
NSString
NSNumber
NSDictionary
NSArray
Ad esempio, puoi aggiungere un utente con setValue
nel seguente modo:
Swift
self.ref.child("users").child(user.uid).setValue(["username": username])
Objective-C
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
L'utilizzo di setValue
in questo modo sovrascrive i dati nella posizione specificata, inclusi tutti i nodi secondari. Tuttavia, puoi comunque aggiornare un figlio senza
riscrivere l'intero oggetto. Se vuoi consentire agli utenti di aggiornare i propri profili,
puoi aggiornare il nome utente nel seguente modo:
Swift
self.ref.child("users/\(user.uid)/username").setValue(username)
Objective-C
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
Lettura di dati
Leggere i dati ascoltando gli eventi di valore
Per leggere i dati in un percorso e ascoltare le modifiche, utilizza
observeEventType:withBlock
di FIRDatabaseReference
per osservare
gli eventi FIRDataEventTypeValue
.
Tipo di evento | Utilizzo tipico |
---|---|
FIRDataEventTypeValue |
Leggi e ascolta le modifiche all'intero contenuto di un percorso. |
Puoi utilizzare l'evento FIRDataEventTypeValue
per leggere i dati in un determinato percorso,
così come esistono al momento dell'evento. Questo metodo viene attivato una volta quando
il listener è collegato e di nuovo ogni volta che i dati, inclusi gli elementi secondari,
cambiano. Alla callback dell'evento viene passato un snapshot
contenente tutti i dati in quella posizione, inclusi i dati secondari. Se non sono presenti dati, lo snapshot restituirà
false
quando chiami exists()
e nil
quando leggi la relativa proprietà value
.
L'esempio seguente mostra un'applicazione di blogging sociale che recupera i dettagli di un post dal database:
Swift
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
Objective-C
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
Il listener riceve un FIRDataSnapshot
che contiene i dati nella posizione specificata
nel database al momento dell'evento nella relativa proprietà value
. Puoi
assegnare i valori al tipo nativo appropriato, ad esempio NSDictionary
.
Se non esistono dati nella posizione, il value
è nil
.
Lettura dei dati una sola volta
Lettura una sola volta utilizzando getData()
L'SDK è progettato per gestire le interazioni con i server di database indipendentemente dal fatto che la tua app sia online o offline.
In genere, devi utilizzare le tecniche di eventi di valore descritte sopra per leggere i dati e ricevere notifiche degli aggiornamenti dei dati dal backend. Queste tecniche riducono l'utilizzo e la fatturazione e sono ottimizzate per offrire ai tuoi utenti la migliore esperienza quando vanno online e offline.
Se hai bisogno dei dati una sola volta, puoi utilizzare getData()
per ottenere uno snapshot dei dati dal database. Se per qualsiasi motivo getData()
non è in grado di restituire il valore del server, il client eseguirà il probing della cache di archiviazione locale e restituirà un errore se il valore non viene ancora trovato.
L'esempio seguente mostra il recupero una sola volta dal database del nome utente pubblico di un utente:
Swift
do { let snapshot = try await ref.child("users/\(uid)/username").getData() let userName = snapshot.value as? String ?? "Unknown" } catch { print(error) }
Objective-C
NSString *userPath = [NSString stringWithFormat:@"users/%@/username", uid]; [[ref child:userPath] getDataWithCompletionBlock:^(NSError * _Nullable error, FIRDataSnapshot * _Nonnull snapshot) { if (error) { NSLog(@"Received an error %@", error); return; } NSString *userName = snapshot.value; }];
L'uso non necessario di getData()
può aumentare l'utilizzo della larghezza di banda e comportare una perdita
di prestazioni, che può essere evitata utilizzando un listener in tempo reale come mostrato
sopra.
Lettura dei dati una sola volta con un osservatore
In alcuni casi, potresti voler che il valore della cache locale venga restituito
immediatamente, anziché controllare se è presente un valore aggiornato sul server. In questi
casi puoi utilizzare observeSingleEventOfType
per ottenere immediatamente i dati dalla
cache del disco locale.
Questo è utile per i dati che devono essere caricati una sola volta e non dovrebbero cambiare frequentemente o richiedere un ascolto attivo. Ad esempio, l'app di blogging negli esempi precedenti utilizza questo metodo per caricare il profilo di un utente quando inizia a scrivere un nuovo post:
Swift
let userID = Auth.auth().currentUser?.uid ref.child("users").child(userID!).observeSingleEvent(of: .value, with: { snapshot in // Get user value let value = snapshot.value as? NSDictionary let username = value?["username"] as? String ?? "" let user = User(username: username) // ... }) { error in print(error.localizedDescription) }
Objective-C
NSString *userID = [FIRAuth auth].currentUser.uid; [[[_ref child:@"users"] child:userID] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { // Get user value User *user = [[User alloc] initWithUsername:snapshot.value[@"username"]]; // ... } withCancelBlock:^(NSError * _Nonnull error) { NSLog(@"%@", error.localizedDescription); }];
Aggiornamento o eliminazione dei dati
Aggiornare campi specifici
Per scrivere contemporaneamente a nodi secondari specifici di un nodo senza sovrascrivere altri
nodi secondari, utilizza il metodo updateChildValues
.
Quando chiami updateChildValues
, puoi aggiornare i valori secondari di livello inferiore
specificando un percorso per la chiave. Se i dati vengono archiviati in più posizioni per migliorare la scalabilità, puoi aggiornare tutte le istanze di questi dati utilizzando la distribuzione dei dati. Ad esempio, un'app
di blog social potrebbe voler creare un post e aggiornarlo contemporaneamente
al feed delle attività recenti e al feed delle attività dell'utente che ha pubblicato il post. A questo scopo, l'applicazione
di blogging utilizza un codice simile a questo:
Swift
guard let key = ref.child("posts").childByAutoId().key else { return } let post = ["uid": userID, "author": username, "title": title, "body": body] let childUpdates = ["/posts/\(key)": post, "/user-posts/\(userID)/\(key)/": post] ref.updateChildValues(childUpdates)
Objective-C
NSString *key = [[_ref child:@"posts"] childByAutoId].key; NSDictionary *post = @{@"uid": userID, @"author": username, @"title": title, @"body": body}; NSDictionary *childUpdates = @{[@"/posts/" stringByAppendingString:key]: post, [NSString stringWithFormat:@"/user-posts/%@/%@/", userID, key]: post}; [_ref updateChildValues:childUpdates];
Questo esempio utilizza childByAutoId
per creare un post nel nodo contenente i post per
tutti gli utenti all'indirizzo /posts/$postid
e recuperare contemporaneamente la chiave con
getKey()
. La chiave può essere utilizzata per creare una seconda voce nei post dell'utente all'indirizzo /user-posts/$userid/$postid
.
Utilizzando questi percorsi, puoi eseguire aggiornamenti simultanei a più posizioni nell'albero JSON con una singola chiamata a updateChildValues
, ad esempio come questo esempio crea il nuovo post in entrambe le posizioni. Gli aggiornamenti simultanei eseguiti in questo modo
sono atomici: o tutti gli aggiornamenti vanno a buon fine o tutti gli aggiornamenti non vanno a buon fine.
Aggiungere un blocco di completamento
Se vuoi sapere quando sono stati inviati i dati, puoi aggiungere un blocco di completamento. Sia setValue
che updateChildValues
accettano un blocco di completamento facoltativo che viene chiamato quando la scrittura è stata eseguita nel database. Questo listener può essere utile per tenere traccia dei dati salvati e di quelli ancora in fase di sincronizzazione. Se la chiamata non è andata a buon fine,
al listener viene passato un oggetto di errore che indica il motivo del problema.
Swift
do { try await ref.child("users").child(user.uid).setValue(["username": username]) print("Data saved successfully!") } catch { print("Data could not be saved: \(error).") }
Objective-C
[[[_ref child:@"users"] child:user.uid] setValue:@{@"username": username} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { if (error) { NSLog(@"Data could not be saved: %@", error); } else { NSLog(@"Data saved successfully."); } }];
Elimina dati
Il modo più semplice per eliminare i dati è chiamare removeValue
su un riferimento alla
posizione di questi dati.
Puoi anche eliminare specificando nil
come valore per un'altra operazione di scrittura
come setValue
o updateChildValues
. Puoi utilizzare questa tecnica
con updateChildValues
per eliminare più figli in una singola chiamata API.
Stacca i listener
Gli osservatori non interrompono automaticamente la sincronizzazione dei dati quando abbandoni un
ViewController
. Se un osservatore non viene rimosso correttamente, continua a sincronizzare
i dati nella memoria locale. Quando un osservatore non è più necessario, rimuovilo passando
il FIRDatabaseHandle
associato al metodo removeObserverWithHandle
.
Quando aggiungi un blocco di callback a un riferimento, viene restituito un FIRDatabaseHandle
.
Questi handle possono essere utilizzati per rimuovere il blocco del callback.
Se a un riferimento al database sono stati aggiunti più listener, ogni listener viene
chiamato quando viene generato un evento. Per interrompere la sincronizzazione dei dati in quella posizione,
devi rimuovere tutti gli osservatori in una posizione chiamando il metodo removeAllObservers
.
La chiamata a removeObserverWithHandle
o removeAllObservers
su un listener non
rimuove automaticamente i listener registrati nei relativi nodi secondari. Devi anche
tenere traccia di questi riferimenti o handle per rimuoverli.
Salvare i dati come transazioni
Quando lavori con dati che potrebbero essere danneggiati da modifiche simultanee, come i contatori incrementali, puoi utilizzare un'operazione di transazione. Fornisci a questa operazione due argomenti: una funzione di aggiornamento e un callback di completamento facoltativo. La funzione di aggiornamento accetta lo stato attuale dei dati come argomento e restituisce il nuovo stato desiderato che vuoi scrivere.
Ad esempio, nell'app di social blogging di esempio, potresti consentire agli utenti di aggiungere e rimuovere le stelle dai post e tenere traccia di quante stelle ha ricevuto un post come segue:
Swift
ref.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in if var post = currentData.value as? [String: AnyObject], let uid = Auth.auth().currentUser?.uid { var stars: [String: Bool] stars = post["stars"] as? [String: Bool] ?? [:] var starCount = post["starCount"] as? Int ?? 0 if let _ = stars[uid] { // Unstar the post and remove self from stars starCount -= 1 stars.removeValue(forKey: uid) } else { // Star the post and add self to stars starCount += 1 stars[uid] = true } post["starCount"] = starCount as AnyObject? post["stars"] = stars as AnyObject? // Set value and report transaction success currentData.value = post return TransactionResult.success(withValue: currentData) } return TransactionResult.success(withValue: currentData) }) { error, committed, snapshot in if let error = error { print(error.localizedDescription) } }
Objective-C
[ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) { NSMutableDictionary *post = currentData.value; if (!post || [post isEqual:[NSNull null]]) { return [FIRTransactionResult successWithValue:currentData]; } NSMutableDictionary *stars = post[@"stars"]; if (!stars) { stars = [[NSMutableDictionary alloc] initWithCapacity:1]; } NSString *uid = [FIRAuth auth].currentUser.uid; int starCount = [post[@"starCount"] intValue]; if (stars[uid]) { // Unstar the post and remove self from stars starCount--; [stars removeObjectForKey:uid]; } else { // Star the post and add self to stars starCount++; stars[uid] = @YES; } post[@"stars"] = stars; post[@"starCount"] = @(starCount); // Set value and report transaction success currentData.value = post; return [FIRTransactionResult successWithValue:currentData]; } andCompletionBlock:^(NSError * _Nullable error, BOOL committed, FIRDataSnapshot * _Nullable snapshot) { // Transaction completed if (error) { NSLog(@"%@", error.localizedDescription); } }];
L'utilizzo di una transazione impedisce che il conteggio delle stelle sia errato se più utenti aggiungono una stella allo stesso post contemporaneamente o se il client disponeva di dati obsoleti. Il valore
contenuto nella classe FIRMutableData
è inizialmente l'ultimo
valore noto del client per il percorso o nil
se non esiste. Il server confronta il valore iniziale con il valore attuale e accetta la transazione se i valori corrispondono o la rifiuta. Se la transazione viene rifiutata, il server restituisce
il valore corrente al client, che esegue nuovamente la transazione con il
valore aggiornato. Questa operazione si ripete finché la transazione non viene accettata o non vengono effettuati troppi
tentativi.
Incrementi lato server atomici
Nello scenario d'uso riportato sopra, scriviamo due valori nel database: l'ID dell'utente che aggiunge/rimuove la stella dal post e il conteggio delle stelle incrementato. Se sappiamo già che l'utente ha aggiunto il post ai preferiti, possiamo utilizzare un'operazione di incremento atomico anziché una transazione.
Swift
let updates = [ "posts/\(postID)/stars/\(userID)": true, "posts/\(postID)/starCount": ServerValue.increment(1), "user-posts/\(postID)/stars/\(userID)": true, "user-posts/\(postID)/starCount": ServerValue.increment(1) ] as [String : Any] Database.database().reference().updateChildValues(updates)
Objective-C
NSDictionary *updates = @{[NSString stringWithFormat: @"posts/%@/stars/%@", postID, userID]: @TRUE, [NSString stringWithFormat: @"posts/%@/starCount", postID]: [FIRServerValue increment:@1], [NSString stringWithFormat: @"user-posts/%@/stars/%@", postID, userID]: @TRUE, [NSString stringWithFormat: @"user-posts/%@/starCount", postID]: [FIRServerValue increment:@1]}; [[[FIRDatabase database] reference] updateChildValues:updates];
Questo codice non utilizza un'operazione di transazione, pertanto non viene eseguito nuovamente in caso di aggiornamento in conflitto. Tuttavia, poiché l'operazione di incremento viene eseguita direttamente sul server di database, non esiste la possibilità di un conflitto.
Se vuoi rilevare e rifiutare conflitti specifici dell'applicazione, ad esempio un utente che aggiunge una stella a un post a cui l'aveva già aggiunta, devi scrivere regole di sicurezza personalizzate per questo caso d'uso.
Lavorare con i dati offline
Se un client perde la connessione di rete, la tua app continuerà a funzionare correttamente.
Ogni client connesso a un database Firebase mantiene la propria versione interna di tutti i dati attivi. Quando i dati vengono scritti, vengono scritti prima in questa versione locale. Il client Firebase sincronizza quindi i dati con i server di database remoti e con altri client in base al "miglior sforzo".
Di conseguenza, tutte le scritture nel database attivano immediatamente gli eventi locali, prima che i dati vengano scritti sul server. Ciò significa che la tua app rimane reattiva indipendentemente dalla latenza o dalla connettività di rete.
Una volta ripristinata la connettività, la tua app riceve il set appropriato di eventi in modo che il client si sincronizzi con lo stato attuale del server, senza dover scrivere codice personalizzato.
Parleremo più nel dettaglio del comportamento offline in Scopri di più sulle funzionalità online e offline.