Ler e gravar dados em plataformas da Apple

Criar protótipos e fazer testes com Firebase Local Emulator Suite (opcional)

Antes de compreender como o app lê e grava no Realtime Database, é importante conhecer um conjunto de ferramentas que podem ser usadas para criar protótipos e testar a funcionalidade do Realtime Database: Firebase Local Emulator Suite. Conseguir trabalhar localmente sem precisar implantar serviços existentes será uma ótima ideia se você estiver testando diferentes modelos de dados, otimizando suas regras de segurança ou procurando a maneira mais econômica de interagir com o back-end.

Um emulador do Realtime Database faz parte do Local Emulator Suite, que permite que o app interaja com o conteúdo e a configuração do banco de dados emulado e também com os recursos do projeto emulado (funções, outros bancos de dados e regras de segurança).

O uso do emulador do Realtime Database envolve apenas algumas etapas:

  1. Para se conectar ao emulador, adicione uma linha de código à configuração de teste do app.
  2. Execute firebase emulators:start na raiz do diretório do projeto local.
  3. Faça chamadas pelo código de protótipo do app usando um SDK da plataforma do Realtime Database, como de costume, ou a API REST do Realtime Database.

Confira um tutorial detalhado sobre o Realtime Database e o Cloud Functions. Consulte também a introdução do Local Emulator Suite.

Como ter uma FIRDatabaseReference

Para ler ou gravar dados no banco de dados, você precisa de uma instância de FIRDatabaseReference:

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
@property (strong, nonatomic) FIRDatabaseReference *ref;

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

Gravar dados

Veja neste documento as noções básicas de leitura e gravação de dados do Firebase.

Esses dados são gravados em uma referência Database e recuperados com um listener assíncrono anexado à referência. Ele é acionado uma vez no estado inicial dos dados, e posteriormente, quando há alterações.

Operações básicas de gravação

Em operações básicas de gravação, use setValue para salvar dados em uma referência específica e substituir os dados no caminho. Use esse método para as seguintes ações:

  • Transmitir os tipos que correspondam aos tipos JSON disponíveis:
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

Por exemplo, é possível adicionar um usuário com setValue da seguinte maneira:

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
self.ref.child("users").child(user.uid).setValue(["username": username])

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

O uso de setValue dessa maneira substitui os dados no local especificado, incluindo qualquer nó filho. No entanto, ainda é possível atualizar um filho sem substituir o objeto inteiro. Para que os usuários atualizem os próprios perfis, atualize o nome deles desta forma:

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
self.ref.child("users/\(user.uid)/username").setValue(username)

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

Ler dados

Ler dados detectando eventos de valores

Para ler dados em um caminho e detectar as alterações, use o observeEventType:withBlock do FIRDatabaseReference para observar os eventos FIRDataEventTypeValue.

Tipo de evento Uso normal
FIRDataEventTypeValue Ler e detectar alterações em todo o conteúdo de um caminho.

É possível usar o evento FIRDataEventTypeValue para ler os dados em um caminho específico, exatamente como estão no momento do evento. Esse método será acionado uma vez quando o listener for anexado, e sempre que houver alteração nos dados, inclusive nos filhos. O retorno de chamada do evento recebe um snapshot contendo todos os dados nesse local, incluindo os dados filhos. Se não houver dados, o snapshot retornará false na chamada de exists() e nil, quando a propriedade value é lida.

O exemplo a seguir mostra um aplicativo de blog social recuperando detalhes de uma postagem do banco de dados:

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

O listener recebe um FIRDataSnapshot que contém os dados no local especificado no banco de dados no momento do evento na propriedade value. É possível atribuir os valores ao tipo nativo apropriado, como NSDictionary. Se não houver dados no local, o value será nil.

Ler dados uma vez

Ler uma vez usando getData()

O SDK foi projetado para gerenciar interações com servidores de banco de dados com seu app on-line ou off-line.

Geralmente, é necessário usar as técnicas de eventos de valores descritas acima para ler dados para receber notificações de atualizações dos dados do back-end. Essas técnicas reduzem o uso e o faturamento e são otimizadas para oferecer aos usuários a melhor experiência on-line e off-line.

Se você precisar dos dados apenas uma vez, poderá usar o getData() para acessar um snapshot dos dados do banco de dados. Se por algum motivo o getData() não conseguir retornar o valor do servidor, o cliente procurará o cache de armazenamento local e retornará um erro se o valor ainda não for encontrado.

Veja no exemplo a seguir a recuperação do nome de usuário público de um usuário uma única vez no banco de dados:

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
do {
  let snapshot = try await ref.child("users/\(uid)/username").getData()
  let userName = snapshot.value as? String ?? "Unknown"
} catch {
  print(error)
}

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
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;
}];

O uso desnecessário de getData() pode aumentar a utilização da largura de banda e causar a perda de desempenho, o que pode ser impedido com um listener em tempo real, conforme mostrado acima.

Ler dados uma vez com um observador

Em alguns casos, é possível que o valor do cache local seja retornado imediatamente, em vez de verificar um valor atualizado no servidor. Nesses casos, use observeSingleEventOfType para receber os dados do cache do disco local imediatamente.

Isso é útil para dados que só precisam ser carregados uma vez, não são alterados com frequência nem exigem detecção ativa. Por exemplo, o app de blog dos exemplos anteriores usa este método para carregar o perfil de um usuário quando ele começa a escrever uma nova postagem:

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
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

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
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);
}];

Atualizar ou excluir dados

Atualizar campos específicos

Para gravar simultaneamente em filhos específicos de um nó sem substituir outros nós filhos, use o método updateChildValues.

Ao chamar updateChildValues, atualize valores de filhos de nível inferior ao especificar um caminho para a chave. Se os dados estiverem armazenados em vários locais para aprimorar a escalonabilidade, atualize todas as instâncias usando a distribuição de dados. Por exemplo, um app de blog social pode criar uma postagem e atualizá-la simultaneamente no feed de atividades recentes e no feed do autor da postagem. Para fazer isso, o aplicativo de blog usa estes códigos:

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
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

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
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];

Esse exemplo usa childByAutoId para criar uma postagem no nó que armazena as postagens para todos os usuários em /posts/$postid e, simultaneamente, recuperar a chave com getKey(). Depois, a chave pode ser usada para criar uma segunda entrada nas postagens do usuário em /user-posts/$userid/$postid.

Com esses caminhos, você faz atualizações simultâneas em vários locais da árvore JSON com uma única chamada ao updateChildValues, da mesma forma que esse exemplo cria a nova postagem nos dois locais. Essas atualizações são atômicas: ou todas funcionam, ou todas falham.

Adicionar um bloco de conclusão

Para saber quando seus dados foram confirmados, você pode adicionar um bloco de conclusão. setValue e updateChildValues recebem um bloco de conclusão opcional que é chamado quando a gravação é confirmada no banco de dados. Esse listener pode ser útil para monitorar quais dados foram salvos e quais dados ainda estão sendo sincronizados. Se a chamada não for bem-sucedida, o listener receberá um objeto de erro indicando o motivo da falha.

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
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

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
[[[_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.");
  }
}];

Excluir dados

A maneira mais simples de excluir os dados é chamar removeValue em uma referência ao local desses dados.

Também é possível fazer a exclusão ao especificar nil como o valor de outra operação de gravação, como setValue ou updateChildValues. Você pode usar essa técnica com updateChildValues para excluir vários filhos em uma única chamada de API.

Remover listeners

Os observadores não interrompem automaticamente a sincronização de dados quando você sai de um ViewController. Se um observador não for removido corretamente, ele continuará sincronizando dados com a memória local. Quando um observador não for mais necessário, remova-o passando o FIRDatabaseHandle associado ao método removeObserverWithHandle.

Ao adicionar um bloco de retorno de chamada a uma referência, um FIRDatabaseHandle é retornado. Esses handles podem ser usados para remover esse bloco.

Se vários listeners são adicionados a uma referência de banco de dados, cada um é chamado quando um evento é acionado. Para interromper a sincronização de dados nesse local, remova todos os observadores em um local chamando o método removeAllObservers.

Chamar removeObserverWithHandle ou removeAllObservers em um listener não remove automaticamente os listeners registrados nos nós filhos. É preciso também encontrar essas referências ou handles para removê-los.

Salvar dados como transações

Ao trabalhar com dados que podem ser corrompidos por modificações simultâneas, como contadores incrementais, use uma operação de transação. Essa operação aceita dois argumentos: uma função de atualização e um callback de conclusão opcional. A função de atualização usa o estado atual dos dados como argumento e retorna o novo estado de acordo com as preferências de gravação.

Por exemplo, os usuários do app de blog social podem adicionar ou remover estrelas de postagens e acompanhar quantas foram recebidas da seguinte maneira:

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
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

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
[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);
  }
}];

A transação impede uma contagem incorreta de estrelas se vários usuários adicionam simultaneamente estrelas à mesma postagem ou se o cliente tem dados desatualizados. Inicialmente, o valor contido na classe FIRMutableData é o último conhecido pelo cliente para o caminho, ou nil se não houver nenhum. O servidor compara o valor inicial com o atual e aceita a transação se eles forem correspondentes. Caso contrário, ele a rejeita. Se a transação é rejeitada, o servidor retorna o valor atual para o cliente, que executa a transação novamente com o valor atualizado. Isso se repetirá até que a transação seja aceita ou até que muitas tentativas sejam realizadas.

Incrementos atômicos do lado do servidor

No caso de uso acima, estamos gravando dois valores no banco de dados: o ID do usuário que marca a publicação com estrela ou remove a marcação e a contagem de estrelas incrementada. Se já soubermos que o usuário está marcando a postagem com estrela, podemos usar uma operação de incremento atômico em vez de uma transação.

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
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

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
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];

Este código não usa uma operação de transação. Portanto, ele não será executado de novo automaticamente se houver uma atualização conflitante. No entanto, como a operação de incremento acontece diretamente no servidor de banco de dados, não há possibilidade de conflito.

Se você quiser detectar e rejeitar conflitos específicos do aplicativo, como um usuário marcando uma postagem que já havia marcado antes, escreva regras de segurança personalizadas para esse caso de uso.

Trabalhar com dados off-line

Se um cliente perder a conexão de rede, o app continuará funcionando.

Todos os clientes conectados a um banco de dados do Firebase mantêm a própria versão interna de dados ativos. A gravação deles ocorre primeiro nessa versão local. Depois, o cliente do Firebase sincroniza esses dados com os servidores remotos e com outros clientes de acordo com o modelo "melhor esforço".

Consequentemente, todas as gravações no banco de dados acionam eventos locais, antes de qualquer dado ser gravado no servidor, e o app continua responsivo, independentemente da conectividade ou da latência da rede.

Para que a conectividade seja restabelecida, seu app recebe o conjunto adequado de eventos, e o cliente faz a sincronização com o estado atual do servidor, sem precisar de um código personalizado.

Confira Saiba mais sobre recursos on-line e off-line se você quiser ver detalhes sobre esse assunto.

Próximas etapas