Ler e gravar dados no iOS

Criar protótipos e fazer testes com o Pacote do emulador local do Firebase (opcional)

Antes de ver informações sobre como um app lê e grava no Realtime Database, confira o Pacote do emulador local do Firebase, um conjunto de ferramentas que podem ser usadas para criar protótipos e testar a funcionalidade do Realtime Database. 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 Pacote do emulador local, que permite ao seu app interagir com o conteúdo e a configuração do banco de dados emulado. Além disso, ele também permite interagir com os recursos do projeto emulado (como 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 aplicativo.
  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 o SDK da plataforma do Realtime Database como você faz geralmente ou usando a API REST desse banco de dados.

Veja neste link um tutorial detalhado sobre o Realtime Database e o Cloud Functions. Consulte também a Introdução ao Pacote do emulador local.

Como ter uma FIRDatabaseReference

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

Swift

var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

@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 FIRDatabase 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

self.ref.child("users").child(user.uid).setValue(["username": username])

Objective-C

[[[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

self.ref.child("users/\(user.uid)/username").setValue(username)

Objective-C

[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

Ler dados

Ler dados detectando eventos de valor

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

refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Objective-C

_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 valor 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.

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

self.ref.child("users/\(user.uid)/username").getData { (error, snapshot) in
    if let error = error {
        print("Error getting data \(error)")
    }
    else if snapshot.exists() {
        print("Got data \(snapshot.value!)")
    }
    else {
        print("No data available")
    }
}

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 de 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

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);
}];

Como 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

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];

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(). É possível usar a chave 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

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!")
  }
}

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.");
  }
}];

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. Use 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 identificadores 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 identificadores 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

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);
  }
}];

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, rejeita a transação. 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 estrelas 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

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];

Este código não usa uma operação de transação. Portanto, ele não será executado automaticamente de maneira automática 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 com estrela, 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