Lettura e scrittura di dati sulle piattaforme Apple

(Facoltativo) Crea un prototipo e testa con Firebase Local Emulator Suite

Prima di parlare di come la tua app legge e scrive in Realtime Database, introduciamo un insieme di strumenti che puoi utilizzare per realizzare prototipi e testare la funzionalità di Realtime Database: Firebase Local Emulator Suite. Se stai provando diversi modelli di dati, ottimizzando le regole di sicurezza o cercando di trovare il modo più economico per interagire con il back-end, la possibilità di lavorare localmente senza implementare i servizi in produzione può essere un'ottima idea.

Un emulatore Realtime Database fa parte di Local Emulator Suite, che consente all'app di interagire con i contenuti e la configurazione del database emulati, nonché, facoltativamente, con le risorse del progetto emulate (funzioni, altri database e regole di sicurezza).

L'utilizzo dell'emulatore Realtime Database prevede solo pochi passaggi:

  1. Aggiungere una riga di codice alla configurazione di test dell'app per connettersi all'emulatore.
  2. Dalla directory principale del progetto locale, esegui firebase emulators:start.
  3. Effettuare chiamate dal codice del prototipo dell'app utilizzando un SDK della piattaforma Realtime Database come di consueto o l'API REST Realtime Database.

È disponibile una procedura dettagliata che coinvolge Realtime Database e Cloud Functions. Consulta anche l'introduzione a Local Emulator Suite.

Ottenere un riferimento a un database FIR

Per leggere o scrivere dati dal database, devi avere un'istanza di FIRDatabaseReference:

Swift

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
@property (strong, nonatomic) FIRDatabaseReference *ref;

self.ref = [[FIRDatabase database] reference];

Scrivi dati

Questo documento illustra le nozioni di base per leggere e scrivere i dati di Firebase.

I dati di Firebase vengono scritti in un riferimento Database e recuperati collegando un ascoltatore asincrono al riferimento. L'ascoltatore 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 eventuali dati esistenti in quel percorso. Puoi utilizzare questo metodo per:

  • Passa i tipi che corrispondono ai tipi JSON disponibili come segue:
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

Ad esempio, puoi aggiungere un utente con setValue come segue:

Swift

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
self.ref.child("users").child(user.uid).setValue(["username": username])

Objective-C

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
[[[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 eventuali nodi secondari. Tuttavia, puoi comunque aggiornare un elemento secondario senza riscrivere l'intero oggetto. Se vuoi consentire agli utenti di aggiornare i propri profili, puoi aggiornare il nome utente come segue:

Swift

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
self.ref.child("users/\(user.uid)/username").setValue(username)

Objective-C

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

Lettura di dati

Leggere i dati ascoltando gli eventi relativi ai valori

Per leggere i dati in un percorso e ascoltare le modifiche, utilizza il metodo observeEventType:withBlock di FIRDatabaseReference per osservare gli eventi FIRDataEventTypeValue.

Tipo di evento Utilizzo tipico
FIRDataEventTypeValue Leggere e ascoltare le modifiche a tutti i contenuti di un percorso.

Puoi utilizzare l'evento FIRDataEventTypeValue per leggere i dati in un determinato percorso, come esistente al momento dell'evento. Questo metodo viene attivato una volta quando l'ascoltatore è collegato e di nuovo ogni volta che i dati, inclusi eventuali elementi secondari, cambiano. Al callback dell'evento viene passato un snapshot contenente tutti i dati in quella posizione, inclusi i dati secondari. Se non sono presenti dati, l'istantanea restituirà false quando chiami exists() e nil quando leggi la relativa proprietà value.

L'esempio seguente mostra un'applicazione di social blogging che recupera i dettagli di un post dal database:

Swift

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Objective-C

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

L'ascoltatore riceve un FIRDataSnapshot che contiene i dati nella posizione specificata nel database al momento dell'evento nella sua proprietà value. Puoi assegnare i valori al tipo nativo appropriato, ad esempio NSDictionary. Se non esistono dati nella posizione, value è nil.

Leggi i dati una volta

Lettura una 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 degli 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 sono 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 esaminerà la cache dello spazio di archiviazione locale e restituirà un errore se il valore non viene ancora trovato.

L'esempio seguente mostra il recupero del nome utente rivolto al pubblico di un utente una sola volta dal database:

Swift

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
do {
  let snapshot = try await ref.child("users/\(uid)/username").getData()
  let userName = snapshot.value as? String ?? "Unknown"
} catch {
  print(error)
}

Objective-C

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
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 rendimento, che può essere evitata utilizzando un ascoltatore in tempo reale come mostrato sopra.

Leggere i dati una 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 recuperare immediatamente i dati dalla cache locale del disco.

Questo è utile per i dati che devono essere caricati una sola volta e che non dovrebbero cambiare frequentemente o richiedere l'ascolto attivo. Ad esempio, l'app di blogging indicata negli esempi precedenti utilizza questo metodo per caricare il profilo di un utente quando inizia a scrivere un nuovo post:

Swift

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
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

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
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 di dati

Aggiornare campi specifici

Per scrivere contemporaneamente in 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 una migliore scalabilità, puoi aggiornare tutte le istanze utilizzando la distribuzione dei dati. Ad esempio, un'app di blogging social potrebbe voler creare un post e aggiornarlo contemporaneamente nel feed delle attività recenti e nel feed delle attività dell'utente che ha pubblicato il post. A tale scopo, l'applicazione di blogging utilizza codice come questo:

Swift

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
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

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
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 in /posts/$postid e contemporaneamente recuperare la chiave con getKey(). La chiave può essere utilizzata per creare una seconda voce nei post dell'utente su /user-posts/$userid/$postid.

Utilizzando questi percorsi, puoi eseguire aggiornamenti simultanei in più posizioni nella struttura ad albero JSON con una singola chiamata a updateChildValues, ad esempio come in questo esempio che 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 non vanno a buon fine.

Aggiungere un blocco di completamento

Se vuoi sapere quando i dati sono stati sottoposti a commit, puoi aggiungere un blocco di completamento. Sia setValue che updateChildValues accettano un blocco di completamento facoltativo che viene chiamato quando la scrittura è stata confermata nel database. Questo ascoltatore può essere utile per tenere traccia dei dati salvati e di quelli in fase di sincronizzazione. Se la chiamata non è andata a buon fine, all'ascoltatore viene passato un oggetto errore che indica il motivo dell'errore.

Swift

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
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

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
[[[_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 loro posizione.

Puoi anche eliminare specificando nil come valore per un'altra operazione di scrittura, ad esempio setValue o updateChildValues. Puoi utilizzare questa tecnica con updateChildValues per eliminare più elementi secondari in una singola chiamata API.

Scollegare gli ascoltatori

Gli osservatori non interrompono automaticamente la sincronizzazione dei dati quando esci da un ViewController. Se un osservatore non viene rimosso correttamente, continua a sincronizzare i dati con la 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 di callback.

Se a un riferimento a un database sono stati aggiunti più ascoltatori, ogni ascoltatore viene chiamato quando viene attivato 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 ascoltatore nonrimuove automaticamente gli ascoltatori registrati sui 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 contemporaneamente, ad esempio contatori incrementali, puoi utilizzare un'operazione di transazione. A questa operazione vengono assegnati due argomenti: una funzione di aggiornamento e una funzione di richiamata facoltativa al completamento. La funzione di aggiornamento prende lo stato attuale dei dati come argomento e restituisce il nuovo stato desiderato che vuoi scrivere.

Ad esempio, nell'esempio di app di social blogging, potresti consentire agli utenti di assegnare e togliere stelle ai post e tenere traccia del numero di stelle ricevute da un post come segue:

Swift

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
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

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
[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 i conteggi delle stelle siano errati se più utenti assegnano una stella allo stesso post contemporaneamente o se il cliente ha dati non aggiornati. Il valore contenuto nella classe FIRMutableData è inizialmente l'ultimo valore conosciuto del cliente per il percorso oppure nil se non esiste. Il server confronta il valore iniziale con il valore corrente e accetta la transazione se i valori corrispondono oppure la rifiuta. Se la transazione viene rifiutata, il server restituisce il valore corrente al client, che esegue di nuovo la transazione con il valore aggiornato. L'operazione viene ripetuta fino a quando non viene accettata o fino a quando non sono stati effettuati troppi tentativi.

Incrementi atomici lato server

Nel caso d'uso riportato sopra, scriviamo due valori nel database: l'ID dell'utente che aggiunge/rimuove una stella al post e il conteggio delle stelle incrementato. Se sappiamo già che l'utente aggiunge il post ai preferiti, possiamo utilizzare un'operazione di incremento atomico anziché una transazione.

Swift

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
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

Nota:questo prodotto Firebase non è disponibile come target di App Clip.
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 automaticamente se è presente un aggiornamento in conflitto. Tuttavia, poiché l'operazione di incremento avviene 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 un post a Speciali dopo averlo già aggiunto in precedenza, devi scrivere regole di sicurezza personalizzate per questo caso d'uso.

Lavorare con i dati offline

Se un client perde la connessione di rete, l'app continuerà a funzionare correttamente.

Ogni client collegato a un database Firebase gestisce la propria versione interna di tutti i dati attivi. Quando i dati vengono scritti, vengono prima scritti in questa versione locale. Il client Firebase sincronizza quindi i dati con i server database remoto e con altri client in base al criterio "best effort".

Di conseguenza, tutte le scritture nel database attivano immediatamente gli eventi locali, prima che qualsiasi dato venga scritto sul server. Ciò significa che la tua app rimane dinamica indipendentemente dalla latenza o dalla connettività di rete.

Una volta ripristinata la connettività, l'app riceve l'insieme appropriato di eventi in modo che il client si sincronizzi con lo stato attuale del server, senza dover scrivere codice personalizzato.

Scopriremo di più sul comportamento offline in Scopri di più sulle funzionalità online e offline.

Passaggi successivi