(Facoltativo) Prototipa e testa con Firebase Local Emulator Suite
Prima di parlare di come la tua app legge e scrive su Realtime Database, introduciamo una serie di strumenti che puoi utilizzare per prototipare e testare la funzionalità di Realtime Database: Firebase Local Emulator Suite. Se stai provando diversi modelli di dati, ottimizzando le regole di sicurezza o lavorando per trovare il modo più conveniente per interagire con il back-end, poter lavorare localmente senza distribuire servizi live può essere un'ottima idea.
Un emulatore di Realtime Database fa parte di Local Emulator Suite, che consente alla tua app di interagire con il contenuto 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 solo pochi passaggi:
- Aggiunta di una riga di codice alla configurazione di test della tua app per connettersi all'emulatore.
- Dalla radice della directory del progetto locale, eseguendo
firebase emulators:start
. - Effettuare chiamate dal codice prototipo della tua app utilizzando l'SDK della piattaforma Realtime Database come di consueto o utilizzando l'API REST di Realtime Database.
È disponibile una procedura dettagliata dettagliata che coinvolge Realtime Database e Cloud Functions . Dovresti anche dare un'occhiata all'introduzione della Local Emulator Suite .
Ottieni un riferimento al database FIR
Per leggere o scrivere dati dal database, è necessaria un'istanza di FIRDatabaseReference
:
Veloce
var ref: DatabaseReference! ref = Database.database().reference()
Obiettivo-C
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
Scrivi dati
Questo documento copre le nozioni di base sulla lettura e scrittura dei dati Firebase.
I dati Firebase vengono scritti in un riferimento Database
e recuperati collegando un ascoltatore asincrono al riferimento. Il listener viene attivato una volta per lo stato iniziale dei dati e nuovamente ogni volta che i dati cambiano.
Operazioni di scrittura di base
Per le operazioni di scrittura di base, è possibile utilizzare setValue
per salvare i dati in un riferimento specificato, sostituendo eventuali dati esistenti in quel percorso. Puoi utilizzare questo metodo per:
- Tipi di passaggio che corrispondono ai tipi JSON disponibili come segue:
-
NSString
-
NSNumber
-
NSDictionary
-
NSArray
-
Ad esempio, puoi aggiungere un utente con setValue
come segue:
Veloce
self.ref.child("users").child(user.uid).setValue(["username": username])
Obiettivo-C
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
L'utilizzo setValue
in questo modo sovrascrive i dati nella posizione specificata, inclusi eventuali nodi figlio. Tuttavia, puoi comunque aggiornare un figlio senza riscrivere l'intero oggetto. Se desideri consentire agli utenti di aggiornare i propri profili, puoi aggiornare il nome utente come segue:
Veloce
self.ref.child("users/\(user.uid)/username").setValue(username)
Obiettivo-C
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
Leggi i dati
Leggere i dati ascoltando gli eventi valore
Per leggere i dati in un percorso e ascoltare le modifiche, utilizzare observeEventType:withBlock
di FIRDatabaseReference
per osservare gli eventi FIRDataEventTypeValue
.
Tipo di evento | Utilizzo tipico |
---|---|
FIRDataEventTypeValue | Leggere e ascoltare le modifiche all'intero contenuto di un percorso. |
È possibile utilizzare l'evento FIRDataEventTypeValue
per leggere i dati in un determinato percorso, così come esiste al momento dell'evento. Questo metodo viene attivato una volta quando l'ascoltatore è collegato e di nuovo ogni volta che i dati, inclusi eventuali figli, cambiano. Al callback dell'evento viene passata uno snapshot
contenente tutti i dati in quella posizione, inclusi i dati figlio. Se non ci sono dati, lo snapshot restituirà false
quando chiami exists()
e nil
quando leggi la sua proprietà value
.
L'esempio seguente mostra un'applicazione di social blogging che recupera i dettagli di un post dal database:
Veloce
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
Obiettivo-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
. È possibile assegnare i valori al tipo nativo appropriato, ad esempio NSDictionary
. Se non esistono dati nella posizione, il value
è nil
.
Leggere i dati una volta
Leggi una volta usando getData()
L'SDK è progettato per gestire le interazioni con i server di database sia che la tua app sia online che offline.
In genere, dovresti utilizzare le tecniche degli eventi valore descritte sopra per leggere i dati e ricevere notifiche sugli aggiornamenti dei dati dal backend. Queste tecniche riducono l'utilizzo e la fatturazione e sono ottimizzate per offrire ai tuoi utenti la migliore esperienza mentre vanno online e offline.
Se hai bisogno dei dati solo una volta, puoi utilizzare getData()
per ottenere un'istantanea dei dati dal database. Se per qualsiasi motivo getData()
non è in grado di restituire il valore del server, il client esaminerà la cache di archiviazione locale e restituirà un errore se il valore non viene ancora trovato.
L'esempio seguente dimostra come recuperare il nome utente pubblico di un utente una sola volta dal database:
Veloce
do { let snapshot = try await ref.child("users/\(uid)/username").getData() let userName = snapshot.value as? String ?? "Unknown" } catch { print(error) }
Obiettivo-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'uso della larghezza di banda e portare a una perdita di prestazioni, che può essere prevenuta utilizzando un ascoltatore in tempo reale come mostrato sopra.
Leggere i dati una volta con un osservatore
In alcuni casi potresti voler restituire immediatamente il valore dalla cache locale, invece di verificare la presenza di un valore aggiornato sul server. In questi casi è possibile observeSingleEventOfType
per ottenere immediatamente i dati dalla cache del disco locale.
Ciò è utile per i dati che devono essere caricati solo una volta e non si prevede che cambino frequentemente o richiedano 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 creare un nuovo post:
Veloce
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) }
Obiettivo-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 cancellazione dei dati
Aggiorna campi specifici
Per scrivere contemporaneamente su figli specifici di un nodo senza sovrascrivere altri nodi figli, utilizzare 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 una migliore scalabilità, puoi aggiornare tutte le istanze di tali dati utilizzando il fan-out dei dati . Ad esempio, un'app di social blogging potrebbe voler creare un post e contemporaneamente aggiornarlo al feed delle attività recenti e al feed delle attività dell'utente che ha pubblicato il post. Per fare ciò, l'applicazione di blog utilizza un codice come questo:
Veloce
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)
Obiettivo-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 post per tutti gli utenti su /posts/$postid
e contemporaneamente recuperare la chiave con getKey()
. La chiave può quindi essere utilizzata per creare una seconda voce nei post dell'utente in /user-posts/$userid/$postid
.
Utilizzando questi percorsi, puoi eseguire aggiornamenti simultanei in più posizioni nell'albero JSON con una singola chiamata a updateChildValues
, come in questo esempio crea il nuovo post in entrambe le posizioni. Gli aggiornamenti simultanei effettuati in questo modo sono atomici: tutti gli aggiornamenti hanno esito positivo oppure tutti gli aggiornamenti falliscono.
Aggiungi un blocco di completamento
Se vuoi sapere quando i tuoi dati sono stati impegnati, puoi aggiungere un blocco di completamento. Sia setValue
che updateChildValues
accettano un blocco di completamento opzionale che viene chiamato quando la scrittura è stata confermata nel database. Questo ascoltatore può essere utile per tenere traccia di quali dati sono stati salvati e quali dati sono ancora in fase di sincronizzazione. Se la chiamata non ha avuto successo, al listener viene passato un oggetto errore che indica il motivo dell'errore.
Veloce
do { try await ref.child("users").child(user.uid).setValue(["username": username]) print("Data saved successfully!") } catch { print("Data could not be saved: \(error).") }
Obiettivo-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 è removeValue
su un riferimento alla posizione di tali dati.
È inoltre possibile eliminare specificando nil
come valore per un'altra operazione di scrittura come setValue
o updateChildValues
. Puoi utilizzare questa tecnica con updateChildValues
per eliminare più elementi secondari in una singola chiamata API.
Distacca gli ascoltatori
Gli osservatori non interrompono automaticamente la sincronizzazione dei dati quando lasci un ViewController
. Se un osservatore non viene rimosso correttamente, continua a sincronizzare i dati nella memoria locale. Quando un osservatore non è più necessario, rimuoverlo passando il FIRDatabaseHandle
associato al removeObserverWithHandle
.
Quando aggiungi un blocco di callback a un riferimento, viene restituito un FIRDatabaseHandle
. Questi handle possono essere utilizzati per rimuovere il blocco di callback.
Se sono stati aggiunti più ascoltatori a un riferimento al database, ogni ascoltatore viene chiamato quando viene generato un evento. Per interrompere la sincronizzazione dei dati in quella posizione, è necessario rimuovere tutti gli osservatori in una posizione chiamando il removeAllObservers
.
La chiamata removeObserverWithHandle
o removeAllObservers
su un ascoltatore non rimuove automaticamente gli ascoltatori registrati sui suoi nodi figli; è inoltre necessario tenere traccia di tali riferimenti o handle per rimuoverli.
Salva i dati come transazioni
Quando si lavora con dati che potrebbero essere danneggiati da modifiche simultanee, come i contatori incrementali, è possibile utilizzare un'operazione di transazione . Assegna a questa operazione due argomenti: una funzione di aggiornamento e un callback di completamento opzionale. La funzione di aggiornamento prende lo stato corrente dei dati come argomento e restituisce il nuovo stato desiderato che desideri scrivere.
Ad esempio, nell'app di social blogging di esempio, potresti consentire agli utenti di aggiungere o rimuovere post da Speciali e tenere traccia di quante stelle ha ricevuto un post come segue:
Veloce
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) } }
Obiettivo-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 contrassegnano contemporaneamente lo stesso post come Speciali o se i dati del client non sono aggiornati. Il valore contenuto nella classe FIRMutableData
è inizialmente l'ultimo valore noto del client per il percorso oppure nil
se non ce n'è. Il server confronta il valore iniziale con il suo valore corrente 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. Ciò si ripete finché la transazione non viene accettata o vengono effettuati troppi tentativi.
Incrementi atomici lato server
Nel caso d'uso precedente stiamo scrivendo due valori nel database: l'ID dell'utente che contrassegna/rimuove il post da Speciali e il numero di stelle incrementato. Se sappiamo già che l'utente è protagonista del post, possiamo utilizzare un'operazione di incremento atomico invece di una transazione.
Veloce
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)
Obiettivo-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, quindi non viene rieseguito automaticamente in caso di aggiornamento in conflitto. Tuttavia, poiché l'operazione di incremento avviene direttamente sul server del database, non vi è alcuna possibilità che si verifichi un conflitto.
Se desideri rilevare e rifiutare conflitti specifici dell'applicazione, ad esempio un utente che contrassegna come Speciale un post che aveva già contrassegnato come Speciale in precedenza, devi scrivere regole di sicurezza personalizzate per quel caso d'uso.
Lavora 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 "massimo sforzo".
Di conseguenza, tutte le scritture nel database attivano immediatamente eventi locali, prima che i dati vengano scritti sul server. Ciò significa che la tua app rimane reattiva indipendentemente dalla latenza di rete o dalla connettività.
Una volta ristabilita la connettività, la tua app riceve il set di eventi appropriato in modo che il client si sincronizzi con lo stato corrente del server, senza dover scrivere alcun codice personalizzato.
Parleremo più approfonditamente del comportamento offline in Ulteriori informazioni sulle funzionalità online e offline .