1. Visão geral
Metas
Neste codelab, você criará um app de recomendação de restaurantes com suporte do Firestore no iOS em Swift. Você aprenderá o seguinte:
- Ler e gravar dados no Firestore usando um app iOS
- Detectar mudanças nos dados do Firestore em tempo real
- Usar o Firebase Authentication e as regras de segurança para proteger os dados do Firestore
- Criar consultas complexas do Firestore
Pré-requisitos
Antes de iniciar este codelab, verifique se você instalou o seguinte:
- Xcode versão 14.0 (ou mais recente)
- CocoaPods 1.12.0 (ou versão mais recente)
2. Criar um projeto do Console do Firebase
Adicionar o Firebase ao projeto
- Acesse o Console do Firebase.
- Selecione Criar novo projeto e nomeie seu projeto como "Firestore iOS Codelab".
3. Fazer o download do projeto de exemplo
Faça o download do código
Comece clonando o projeto de exemplo e executando pod update
no diretório do projeto:
git clone https://github.com/firebase/friendlyeats-ios cd friendlyeats-ios pod update
Abra FriendlyEats.xcworkspace
no Xcode e execute-o (Cmd+R). O app precisa ser compilado corretamente e falhar imediatamente na inicialização, já que está faltando um arquivo GoogleService-Info.plist
. Vamos corrigir isso na próxima etapa.
Configurar o Firebase
Siga a documentação para criar um novo projeto do Firestore. Depois de criar o projeto, faça o download do arquivo GoogleService-Info.plist
dele no Console do Firebase e arraste-o para a raiz do projeto do Xcode. Execute o projeto novamente para garantir que o app seja configurado corretamente e não falhe mais na inicialização. Depois de fazer login, uma tela em branco vai aparecer, como no exemplo abaixo. Se não for possível fazer login, verifique se você ativou o método de login por e-mail/senha no console do Firebase em "Autenticação".
4. Gravar dados no Firestore
Nesta seção, vamos gravar alguns dados no Firestore para preencher a interface do app. Isso pode ser feito manualmente no Console do Firebase, mas vamos fazer isso no próprio app para demonstrar uma gravação básica do Firestore.
O principal objeto do modelo no app é um restaurante. Os dados do Firestore são divididos em documentos, coleções e subcoleções. Armazenaremos cada restaurante como um documento em uma coleção de nível superior chamada restaurants
. Para saber mais sobre o modelo de dados do Firestore, acesse documentos e coleções na documentação.
Antes de adicionar dados ao Firestore, precisamos obter uma referência à coleção de restaurantes. Adicione o código abaixo à repetição "for" interna no método RestaurantsTableViewController.didTapPopulateButton(_:)
.
let collection = Firestore.firestore().collection("restaurants")
Agora que temos uma referência de coleção, podemos gravar alguns dados. Adicione o seguinte logo após a última linha de código que adicionamos:
let collection = Firestore.firestore().collection("restaurants")
// ====== ADD THIS ======
let restaurant = Restaurant(
name: name,
category: category,
city: city,
price: price,
ratingCount: 0,
averageRating: 0
)
collection.addDocument(data: restaurant.dictionary)
O código acima adiciona um novo documento à coleção de restaurantes. Os dados do documento vêm de um dicionário, que obtemos de um struct de restaurante.
Estamos quase lá. Antes de gravar documentos no Firestore, precisamos abrir as regras de segurança dele e descrever quais partes do banco de dados podem ser gravadas por quais usuários. Por enquanto, vamos permitir que apenas usuários autenticados leiam e gravem em todo o banco de dados. Isso é um pouco permissivo demais para um app de produção, mas durante o processo de criação do app queremos algo relaxado o suficiente para não ter problemas de autenticação constantemente durante os experimentos. No final deste codelab, vamos falar sobre como reforçar suas regras de segurança e limitar a possibilidade de leituras e gravações não intencionais.
Na guia Regras do Console do Firebase, adicione as regras a seguir e clique em Publicar.
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { // // WARNING: These rules are insecure! We will replace them with // more secure rules later in the codelab // allow read, write: if request.auth != null; } } }
Vamos discutir as regras de segurança em detalhes mais tarde, mas, se você estiver com pressa, consulte a documentação sobre as regras de segurança.
Execute o app e faça login. Depois, toque no botão Preencher no canto superior esquerdo, o que criará um lote de documentos do restaurante, embora você ainda não veja isso no aplicativo.
Em seguida, acesse a guia de dados do Firestore no console do Firebase. Agora você verá novas entradas na coleção "Restaurantes":
Parabéns! Você acabou de gravar dados no Firestore usando um app iOS. Na próxima seção, você vai aprender a recuperar dados do Firestore e exibi-los no app.
5. Mostrar dados do Firestore
Nesta seção, você vai aprender a recuperar dados do Firestore e exibi-los no app. As duas etapas principais são criar uma consulta e adicionar um listener de snapshot. Esse listener será notificado sobre todos os dados existentes que correspondem à consulta e receberá atualizações em tempo real.
Primeiro, vamos criar a consulta que exibirá a lista não filtrada de restaurantes padrão. Confira a implementação de RestaurantsTableViewController.baseQuery()
:
return Firestore.firestore().collection("restaurants").limit(to: 50)
Essa consulta recupera até 50 restaurantes da coleção de nível superior chamada "restaurantes". Agora que temos uma consulta, precisamos anexar um listener de snapshot para carregar dados do Firestore em nosso app. Adicione o código abaixo ao método RestaurantsTableViewController.observeQuery()
logo após a chamada para stopObserving()
.
listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
guard let snapshot = snapshot else {
print("Error fetching snapshot results: \(error!)")
return
}
let models = snapshot.documents.map { (document) -> Restaurant in
if let model = Restaurant(dictionary: document.data()) {
return model
} else {
// Don't use fatalError here in a real app.
fatalError("Unable to initialize type \(Restaurant.self) with dictionary \(document.data())")
}
}
self.restaurants = models
self.documents = snapshot.documents
if self.documents.count > 0 {
self.tableView.backgroundView = nil
} else {
self.tableView.backgroundView = self.backgroundView
}
self.tableView.reloadData()
}
O código acima faz o download da coleção do Firestore e a armazena localmente em uma matriz. A chamada addSnapshotListener(_:)
adiciona um listener de snapshot à consulta, que vai atualizar o controlador de visualização sempre que os dados mudarem no servidor. As atualizações são recebidas automaticamente, e não precisamos fazer alterações manualmente. Esse listener de snapshot pode ser invocado a qualquer momento como resultado de uma mudança no lado do servidor. Portanto, é importante que nosso app consiga processar as mudanças.
Depois de mapear nossos dicionários para structs (consulte Restaurant.swift
), mostrar os dados é apenas uma questão de atribuir algumas propriedades de visualização. Adicione as linhas abaixo a RestaurantTableViewCell.populate(restaurant:)
em RestaurantsTableViewController.swift
.
nameLabel.text = restaurant.name
cityLabel.text = restaurant.city
categoryLabel.text = restaurant.category
starsView.rating = Int(restaurant.averageRating.rounded())
priceLabel.text = priceString(from: restaurant.price)
Esse método de preenchimento é chamado pelo método tableView(_:cellForRowAtIndexPath:)
da fonte de dados da visualização de tabela, que cuida do mapeamento da coleção de tipos de valor de antes para as células individuais da visualização de tabela.
Execute o app novamente e verifique se os restaurantes que vimos anteriormente no console estão visíveis no simulador ou dispositivo. Se você concluiu esta seção, seu app agora está lendo e gravando dados com o Cloud Firestore.
6. Como classificar e filtrar dados
No momento, nosso app mostra uma lista de restaurantes, mas o usuário não consegue filtrar com base nas necessidades dele. Nesta seção, você vai usar a consulta avançada do Firestore para ativar o uso de filtros.
Aqui está um exemplo de uma consulta simples para buscar todos os restaurantes de dim sum:
let filteredQuery = query.whereField("category", isEqualTo: "Dim Sum")
Como o nome indica, com o método whereField(_:isEqualTo:)
, a consulta faz o download apenas de membros da coleção com campos que atendam às restrições definidas. Nesse caso, ele só faz o download de restaurantes em que a category
é "Dim Sum"
.
Nesse app, o usuário pode encadear vários filtros para criar consultas específicas, como "Pizza em São Paulo" ou "Frutos do mar em São Paulo, ordenados por popularidade".
Abra RestaurantsTableViewController.swift
e adicione o seguinte bloco de código ao meio de query(withCategory:city:price:sortBy:)
:
if let category = category, !category.isEmpty {
filtered = filtered.whereField("category", isEqualTo: category)
}
if let city = city, !city.isEmpty {
filtered = filtered.whereField("city", isEqualTo: city)
}
if let price = price {
filtered = filtered.whereField("price", isEqualTo: price)
}
if let sortBy = sortBy, !sortBy.isEmpty {
filtered = filtered.order(by: sortBy)
}
O snippet acima adiciona várias cláusulas whereField
e order
para criar uma única consulta composta com base na entrada do usuário. Agora, a consulta vai retornar apenas restaurantes que atendem aos requisitos do usuário.
Execute seu projeto e verifique se você consegue filtrar por preço, cidade e categoria (digite a categoria e os nomes das cidades corretamente). Durante os testes, você pode encontrar erros nos registros semelhantes a este:
Error fetching snapshot results: Error Domain=io.grpc Code=9 "The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_composite=..." UserInfo={NSLocalizedDescription=The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_composite=...}
Isso ocorre porque o Firestore requer índices para a maioria das consultas compostas. A exigência de índices em consultas mantém o Firestore rápido em escala. Abrir o link a partir da mensagem de erro abrirá automaticamente a interface de criação do índice no Console do Firebase com os parâmetros corretos preenchidos. Para saber mais sobre índices no Firestore, acesse a documentação.
7. Gravar dados em uma transação
Nesta seção, vamos adicionar um recurso para que os usuários avaliem os restaurantes. Até agora, todas as nossas gravações foram atômicas e relativamente simples. Se alguma delas apresentar um erro, provavelmente solicitaremos que o usuário tente novamente ou faremos isso automaticamente.
Para adicionar uma classificação a um restaurante, precisamos coordenar várias leituras e gravações. Primeiro, a avaliação em si precisa ser enviada e, em seguida, a contagem de avaliações e a classificação média do restaurante precisam ser atualizadas. Se uma delas falhar, mas a outra não, você ficará em um estado inconsistente em que os dados de uma parte do banco de dados não correspondem aos da outra.
Felizmente, o Firestore oferece uma funcionalidade de transação que nos permite realizar várias leituras e gravações em uma única operação atômica, garantindo a consistência dos dados.
Adicione o código abaixo abaixo de todas as declarações let em RestaurantDetailViewController.reviewController(_:didSubmitFormWithReview:)
.
let firestore = Firestore.firestore()
firestore.runTransaction({ (transaction, errorPointer) -> Any? in
// Read data from Firestore inside the transaction, so we don't accidentally
// update using stale client data. Error if we're unable to read here.
let restaurantSnapshot: DocumentSnapshot
do {
try restaurantSnapshot = transaction.getDocument(reference)
} catch let error as NSError {
errorPointer?.pointee = error
return nil
}
// Error if the restaurant data in Firestore has somehow changed or is malformed.
guard let data = restaurantSnapshot.data(),
let restaurant = Restaurant(dictionary: data) else {
let error = NSError(domain: "FireEatsErrorDomain", code: 0, userInfo: [
NSLocalizedDescriptionKey: "Unable to write to restaurant at Firestore path: \(reference.path)"
])
errorPointer?.pointee = error
return nil
}
// Update the restaurant's rating and rating count and post the new review at the
// same time.
let newAverage = (Float(restaurant.ratingCount) * restaurant.averageRating + Float(review.rating))
/ Float(restaurant.ratingCount + 1)
transaction.setData(review.dictionary, forDocument: newReviewReference)
transaction.updateData([
"numRatings": restaurant.ratingCount + 1,
"avgRating": newAverage
], forDocument: reference)
return nil
}) { (object, error) in
if let error = error {
print(error)
} else {
// Pop the review controller on success
if self.navigationController?.topViewController?.isKind(of: NewReviewViewController.self) ?? false {
self.navigationController?.popViewController(animated: true)
}
}
}
No bloco de atualização, todas as operações que fazemos usando o objeto de transação são tratadas como uma única atualização atômica pelo Firestore. Se a atualização falhar no servidor, o Firestore vai tentar novamente algumas vezes automaticamente. Isso significa que a condição de erro provavelmente é um único erro que ocorre repetidamente, por exemplo, se o dispositivo estiver completamente off-line ou se o usuário não tiver autorização para gravar no caminho em que está tentando gravar.
8. Regras de segurança
Os usuários do nosso app não podem ler e gravar todos os dados no banco de dados. Por exemplo, todos podem ver as avaliações de um restaurante, mas apenas um usuário autenticado pode postar uma avaliação. Não basta escrever um bom código no cliente. Precisamos especificar nosso modelo de segurança de dados no back-end para que ele seja totalmente seguro. Nesta seção, vamos aprender a usar as regras de segurança do Firebase para proteger nossos dados.
Primeiro, vamos analisar as regras de segurança que criamos no início do codelab. Abra o console do Firebase e navegue até Banco de dados > Regras na guia Firestore
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
// Only authenticated users can read or write data
allow read, write: if request.auth != null;
}
}
}
A variável request
nas regras acima é uma variável global disponível em todas as regras, e a condicional que adicionamos garante que a solicitação seja autenticada antes de permitir que os usuários façam qualquer coisa. Isso impede que usuários não autenticados usem a API Firestore para fazer alterações não autorizadas nos seus dados. Esse é um bom começo, mas podemos usar as regras do Firestore para fazer coisas muito mais poderosas.
Vamos restringir as gravações de avaliações para que o ID do usuário da avaliação precise corresponder ao ID do usuário autenticado. Isso impede que os usuários falsifiquem a identidade uns dos outros e deixem avaliações fraudulentas. Substitua suas regras de segurança pelas seguintes:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /restaurants/{any}/ratings/{rating} {
// Users can only write ratings with their user ID
allow read;
allow write: if request.auth != null
&& request.auth.uid == request.resource.data.userId;
}
match /restaurants/{any} {
// Only authenticated users can read or write data
allow read, write: if request.auth != null;
}
}
}
A primeira instrução de correspondência corresponde à subcoleção chamada ratings
de qualquer documento que pertença à coleção restaurants
. A condição allow write
impede que qualquer avaliação seja enviada se o ID do usuário da avaliação não corresponder ao do usuário. A segunda instrução de correspondência permite que qualquer usuário autenticado leia e grave restaurantes no banco de dados.
Isso funciona muito bem para nossas avaliações, já que usamos regras de segurança para declarar explicitamente a garantia implícita que escrevemos no app anteriormente: os usuários só podem escrever as próprias avaliações. Se adicionarmos uma função de edição ou exclusão para as avaliações, esse mesmo conjunto de regras também impedirá que os usuários modifiquem ou excluam as avaliações de outras pessoas. No entanto, as regras do Firestore também podem ser usadas de maneira mais granular para limitar as gravações em campos individuais em documentos, em vez de em documentos inteiros. Podemos usar isso para permitir que os usuários atualizem somente as classificações, a classificação média e o número de avaliações de um restaurante, eliminando a possibilidade de um usuário malicioso alterar o nome ou o local de um restaurante.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /restaurants/{restaurant} {
match /ratings/{rating} {
allow read: if request.auth != null;
allow write: if request.auth != null
&& request.auth.uid == request.resource.data.userId;
}
allow read: if request.auth != null;
allow create: if request.auth != null;
allow update: if request.auth != null
&& request.resource.data.name == resource.data.name
&& request.resource.data.city == resource.data.city
&& request.resource.data.price == resource.data.price
&& request.resource.data.category == resource.data.category;
}
}
}
Aqui, dividimos a permissão de gravação em criação e atualização para ser mais específico sobre quais operações devem ser permitidas. Qualquer usuário pode gravar restaurantes no banco de dados, preservando a funcionalidade do botão "Preencher" que criamos no início do codelab. No entanto, depois que um restaurante é gravado, não é possível mudar o nome, a localização, o preço e a categoria dele. Mais especificamente, a última regra exige que qualquer operação de atualização de restaurante mantenha o mesmo nome, cidade, preço e categoria dos campos já existentes no banco de dados.
Para saber mais sobre o que é possível fazer com as regras de segurança, consulte a documentação.
9. Conclusão
Neste codelab, você aprendeu a fazer leituras e gravações básicas e avançadas com o Firestore, além de proteger o acesso aos dados com regras de segurança. Você pode encontrar a solução completa na ramificação codelab-complete
.
Para saber mais sobre o Firestore, acesse os seguintes recursos: