Leggi e scrivi dati su piattaforme Apple

(Facoltativo) Prototipazione e test con Firebase Local Emulator Suite

Prima di parlare di come la tua app legge e scrive in 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 tue regole di sicurezza o lavorando per trovare il modo più conveniente per interagire con il back-end, essere in grado di lavorare localmente senza distribuire servizi live può essere un'ottima idea.

Un emulatore di database in tempo reale fa parte di Local Emulator Suite, che consente all'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 di database in tempo reale richiede solo pochi passaggi:

  1. Aggiunta di una riga di codice alla configurazione di test dell'app per la connessione all'emulatore.
  2. Dalla radice della directory del tuo progetto locale, eseguendo firebase emulators:start .
  3. Effettuare chiamate dal codice prototipo della tua app utilizzando un SDK della piattaforma Realtime Database come al solito o utilizzando l'API REST di Realtime Database.

È disponibile una procedura dettagliata che coinvolge il database in tempo reale e le funzioni cloud . Dovresti anche dare un'occhiata all'introduzione di Local Emulator Suite .

Ottieni un FIRDatabaseReference

Per leggere o scrivere dati dal database, è necessaria un'istanza di FIRDatabaseReference :

Veloce

Nota: questo prodotto Firebase non è disponibile nella destinazione App Clip.
var ref: DatabaseReference!

ref = Database.database().reference()

Obiettivo-C

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

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

Scrivi dati

Questo documento copre le basi della lettura e della scrittura dei dati di Firebase.

I dati di Firebase vengono scritti in un riferimento al 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 usare setValue per salvare i dati in un riferimento specifico, sostituendo tutti i dati esistenti in quel percorso. Puoi usare 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:

Veloce

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

Obiettivo-C

Nota: questo prodotto Firebase non è disponibile nella destinazione App Clip.
[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

L'uso di 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 vuoi consentire agli utenti di aggiornare i loro profili, puoi aggiornare il nome utente come segue:

Veloce

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

Obiettivo-C

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

Leggi i dati

Leggere i dati ascoltando gli eventi di valore

Per leggere i dati in un percorso e ascoltare le modifiche, utilizzare il 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, poiché 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 passato 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 ne leggi la proprietà value .

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

Veloce

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

Obiettivo-C

Nota: questo prodotto Firebase non è disponibile nella destinazione App Clip.
_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 .

Leggi i dati una volta

Leggi una volta usando getData()

L'SDK è progettato per gestire le interazioni con i server di database indipendentemente dal fatto che l'app sia online o offline.

In genere, dovresti usare le tecniche degli eventi di valore descritte sopra per leggere i dati e ricevere notifiche sugli aggiornamenti dei dati dal back-end. Queste tecniche riducono l'utilizzo e la fatturazione e sono ottimizzate per offrire ai tuoi utenti la migliore esperienza mentre navigano online e offline.

Se hai bisogno dei dati solo una volta, puoi usare 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 di archiviazione locale e restituirà un errore se il valore non viene ancora trovato.

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

Veloce

Nota: questo prodotto Firebase non è disponibile nella destinazione App Clip.
ref.child("users/\(uid)/username").getData(completion:  { error, snapshot in
  guard error == nil else {
    print(error!.localizedDescription)
    return;
  }
  let userName = snapshot.value as? String ?? "Unknown";
});

Obiettivo-C

Nota: questo prodotto Firebase non è disponibile nella destinazione 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'uso della larghezza di banda e portare a una perdita di prestazioni, che può essere prevenuta utilizzando un listener in tempo reale come mostrato sopra.

Leggi i dati una volta con un osservatore

In alcuni casi potresti voler restituire immediatamente il valore dalla cache locale, invece di controllare un valore aggiornato sul server. In questi casi è possibile utilizzare 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 dovrebbero cambiare frequentemente o richiedere un ascolto attivo. Ad esempio, l'app di blog negli esempi precedenti utilizza questo metodo per caricare il profilo di un utente quando inizia a creare un nuovo post:

Veloce

Nota: questo prodotto Firebase non è disponibile nella destinazione 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)
}

Obiettivo-C

Nota: questo prodotto Firebase non è disponibile nella destinazione 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 cancellazione dei dati

Aggiorna campi specifici

Per scrivere contemporaneamente su elementi figlio specifici di un nodo senza sovrascrivere altri nodi figlio, utilizzare il metodo updateChildValues .

Quando si chiama updateChildValues ​​, è possibile aggiornare i valori figlio 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 aggiornarlo contemporaneamente al feed delle attività recenti e al feed delle attività dell'utente che pubblica il post. Per fare ciò, l'applicazione di blog utilizza un codice come questo:

Veloce

Nota: questo prodotto Firebase non è disponibile nella destinazione 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)

Obiettivo-C

Nota: questo prodotto Firebase non è disponibile nella destinazione 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 usa childByAutoId per creare un post nel nodo contenente post per tutti gli utenti in /posts/$postid e recuperare contemporaneamente la chiave con getKey() . La chiave può quindi essere utilizzata per creare una seconda voce nei post dell'utente in /user-posts/$userid/$postid .

Usando 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 effettuati in questo modo sono atomici: tutti gli aggiornamenti riescono o tutti gli aggiornamenti falliscono.

Aggiungi un blocco di completamento

Se vuoi sapere quando i tuoi dati sono stati confermati, puoi aggiungere un blocco di completamento. Sia setValue che updateChildValues ​​accettano un blocco di completamento facoltativo che viene chiamato quando la scrittura è stata salvata nel database. Questo listener può essere utile per tenere traccia di quali dati sono stati salvati e quali dati sono ancora sincronizzati. Se la chiamata non ha avuto successo, al listener viene passato un oggetto di errore che indica il motivo per cui si è verificato l'errore.

Veloce

Nota: questo prodotto Firebase non è disponibile nella destinazione App Clip.
ref.child("users").child(user.uid).setValue(["username": username]) {
  (error:Error?, ref:DatabaseReference) in
  if let error = error {
    print("Data could not be saved: \(error).")
  } else {
    print("Data saved successfully!")
  }
}

Obiettivo-C

Nota: questo prodotto Firebase non è disponibile nella destinazione 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 consiste nel chiamare removeValue su un riferimento alla posizione di tali dati.

Puoi anche eliminare specificando nil come valore per un'altra operazione di scrittura come setValue o updateChildValues ​​. Puoi usare questa tecnica con updateChildValues ​​per eliminare più elementi figlio in una singola chiamata API.

Distacca 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, rimuoverlo passando il FIRDatabaseHandle associato al metodo removeObserverWithHandle .

Quando si aggiunge 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ù listener a un riferimento al database, 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 removeObserverWithHandle o removeAllObservers su un listener non rimuove automaticamente i listener registrati sui suoi nodi figlio; devi anche tenere traccia di quei riferimenti o handle per rimuoverli.

Salva i dati come transazioni

Quando si lavora con dati che potrebbero essere danneggiati da modifiche simultanee, ad esempio 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 si desidera scrivere.

Ad esempio, nell'app di social blogging di esempio, potresti consentire agli utenti di aggiungere e rimuovere i post dagli Speciali e tenere traccia di quante stelle ha ricevuto un post come segue:

Veloce

Nota: questo prodotto Firebase non è disponibile nella destinazione 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)
  }
}

Obiettivo-C

Nota: questo prodotto Firebase non è disponibile nella destinazione 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 il conteggio delle stelle non sia corretto se più utenti contrassegnano lo stesso post contemporaneamente o se il cliente disponeva di dati non aggiornati. Il valore contenuto nella classe FIRMutableData è inizialmente l'ultimo valore noto del client per il percorso o nil se non ce n'è. Il server confronta il valore iniziale con il suo 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 nuovamente la transazione con il valore aggiornato. Questo si ripete fino a quando la transazione non viene accettata o sono stati effettuati troppi tentativi.

Incrementi atomici lato server

Nel caso d'uso sopra, scriviamo due valori nel database: l'ID dell'utente che contrassegna/deseleziona il post e il numero di stelle incrementato. Se sappiamo già che l'utente ha come protagonista il post, possiamo utilizzare un'operazione di incremento atomico invece di una transazione.

Veloce

Nota: questo prodotto Firebase non è disponibile nella destinazione 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);

Obiettivo-C

Nota: questo prodotto Firebase non è disponibile nella destinazione 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, quindi non viene eseguito automaticamente se è presente un aggiornamento in conflitto. Tuttavia, poiché l'operazione di incremento avviene direttamente sul server di database, non vi è alcuna possibilità di conflitto.

Se desideri rilevare e rifiutare conflitti specifici dell'applicazione, ad esempio un utente che ha inserito un post come Speciale in precedenza, dovresti scrivere regole di sicurezza personalizzate per quel caso d'uso.

Lavora con i dati offline

Se un client perde la connessione di rete, l'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 eventi locali immediatamente, 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à, l'app riceve il set di eventi appropriato in modo che il client si sincronizzi con lo stato corrente del server, senza dover scrivere codice personalizzato.

Parleremo di più sul comportamento offline in Ulteriori informazioni sulle funzionalità online e offline .

Prossimi passi