Cloud Firestore iOS Codelab

1. Visão Geral

Metas

Neste codelab, você construirá um aplicativo de recomendação de restaurante apoiado pelo Firestore no iOS no Swift. Você vai aprender como:

  1. Ler e gravar dados no Firestore de um aplicativo iOS
  2. Ouça as alterações nos dados do Firestore em tempo real
  3. Use Firebase Authentication e regras de segurança para proteger os dados do Firestore
  4. Escreva consultas complexas do Firestore

Pré-requisitos

Antes de iniciar este codelab, certifique-se de ter instalado:

  • Xcode versão 8.3 (ou superior)
  • CocoaPods 1.2.1 (ou superior)

2. Crie um projeto de console do Firebase

Adicione o Firebase ao projeto

  1. Vá para a consola Firebase .
  2. Selecione Criar Novo projeto e nomear seu projeto "Firestore iOS Codelab".

3. Obtenha o Projeto de Amostra

Baixe o código

Comece por clonagem do projeto de exemplo e executar pod update no diretório do projeto:

git clone https://github.com/firebase/friendlyeats-ios
cd friendlyeats-ios
pod update

Abrir FriendlyEats.xcworkspace no Xcode e executá-lo (Cmd + R). O aplicativo deve compilar corretamente e cair imediatamente no lançamento, uma vez que está faltando um GoogleService-Info.plist arquivo. Corrigiremos isso na próxima etapa.

Configure o Firebase

Siga a documentação para criar um novo projeto Firestore. Assim que tiver o seu projeto, faça o download do seu projeto GoogleService-Info.plist arquivo a partir de Firebase consola e arraste-o para a raiz do projeto Xcode. Execute o projeto novamente para garantir que o aplicativo seja configurado corretamente e não trava mais na inicialização. Após o login, você deverá ver uma tela em branco como no exemplo abaixo. Se você não conseguir fazer login, certifique-se de ter ativado o método de login de e-mail / senha no Firebase console em Autenticação.

10a0671ce8f99704.png

4. Grave dados no Firestore

Nesta seção, escreveremos alguns dados no Firestore para que possamos preencher a IU do aplicativo. Isso pode ser feito manualmente através da consola Firebase , mas vamos fazê-lo no próprio aplicativo para demonstrar uma gravação básica Firestore.

O principal objeto de modelo em nosso aplicativo é um restaurante. Os dados do Firestore são divididos em documentos, coleções e subcoleções. Nós iremos guardar cada restaurante como um documento em uma coleção de nível superior chamada restaurants . Se você gostaria de saber mais sobre o modelo de dados Firestore, ler sobre documentos e coleções em documentação .

Antes de adicionarmos dados ao Firestore, precisamos obter uma referência para a coleção de restaurantes. Adicionar o seguinte para o interior para o laço no RestaurantsTableViewController.didTapPopulateButton(_:) método.

let collection = Firestore.firestore().collection("restaurants")

Agora que temos uma referência de coleção, podemos escrever 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 uma estrutura de restaurante.

Estamos quase lá - antes de podermos escrever documentos para o Firestore, precisamos abrir as regras de segurança do Firestore e descrever quais partes de nosso banco de dados devem ser gravadas por quais usuários. Por enquanto, permitiremos que apenas usuários autenticados leiam e gravem em todo o banco de dados. Isso é um pouco permissivo demais para um aplicativo de produção, mas durante o processo de criação do aplicativo, queremos algo relaxado o suficiente para que não tenhamos problemas de autenticação constantes durante os experimentos. No final deste codelab, falaremos sobre como fortalecer suas regras de segurança e limitar a possibilidade de leituras e gravações indesejadas.

Na guia Regras do console Firebase adicione as seguintes regras e, em seguida, 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 regras de segurança em detalhe mais tarde, mas se você estiver com pressa, dê uma olhada na documentação regras de segurança .

Execute o aplicativo e faça login. Em seguida, toque no botão "Preencher" no canto superior esquerdo, que irá criar um lote de documentos restaurante, embora você não vai ver isso no app ainda.

Em seguida, vá até a guia dados Firestore no console Firebase. Agora você deve ver novas entradas na coleção de restaurantes:

Captura de tela 06-07-2017 às 12.45.38 PM.png

Parabéns, você acabou de gravar dados no Firestore de um aplicativo iOS! Na próxima seção, você aprenderá como recuperar dados do Firestore e exibi-los no aplicativo.

5. Exibir dados do Firestore

Nesta seção, você aprenderá como recuperar dados do Firestore e exibi-los no aplicativo. As duas etapas principais são criar uma consulta e adicionar um ouvinte de instantâneo. Este ouvinte será notificado de todos os dados existentes que correspondem à consulta e receberá atualizações em tempo real.

Primeiro, vamos construir a consulta que servirá a lista padrão não filtrada de restaurantes. Dê uma olhada na implementação de RestaurantsTableViewController.baseQuery() :

return Firestore.firestore().collection("restaurants").limit(to: 50)

Esta consulta recupera até 50 restaurantes da coleção de nível superior denominada "restaurantes". Agora que temos uma consulta, precisamos anexar um ouvinte de instantâneo para carregar dados do Firestore em nosso aplicativo. Adicione o seguinte código para a RestaurantsTableViewController.observeQuery() método apenas 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 baixa a coleção do Firestore e a armazena em um array localmente. O addSnapshotListener(_:) chamada adiciona um ouvinte instantâneo para a consulta que irá atualizar o controlador de vista cada vez que as alterações de dados no servidor. Recebemos atualizações automaticamente e não precisamos enviar alterações manualmente. Lembre-se de que esse ouvinte de instantâneo pode ser chamado a qualquer momento como resultado de uma alteração do lado do servidor, portanto, é importante que nosso aplicativo possa lidar com as alterações.

Depois de mapear os nossos dicionários para estruturas (ver Restaurant.swift ), exibindo os dados é apenas uma questão de atribuir algumas propriedades de vista. Adicione as seguintes linhas para RestaurantTableViewCell.populate(restaurant:) no 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)

Este método populate é chamado a partir da fonte de dados de exibição de tabela tableView(_:cellForRowAtIndexPath:) método, que cuida do mapeamento da coleção de tipos de valor de antes para as células vista de tabela individuais.

Execute o aplicativo novamente e verifique se os restaurantes que vimos anteriormente no console agora estão visíveis no simulador ou dispositivo. Se você concluiu esta seção com sucesso, seu aplicativo agora está lendo e gravando dados com o Cloud Firestore!

2ca7f8c6052f7f79.png

6. Classificação e filtragem de dados

Atualmente nosso aplicativo exibe uma lista de restaurantes, mas não há como o usuário filtrar com base em suas necessidades. Nesta seção, você usará a consulta avançada do Firestore para habilitar a filtragem.

Aqui está um exemplo de uma consulta simples para buscar todos os restaurantes Dim Sum:

let filteredQuery = query.whereField("category", isEqualTo: "Dim Sum")

Como o próprio nome indica, o whereField(_:isEqualTo:) método irá fazer o nosso download consulta apenas os membros da coleção cujos campos atender as restrições que estabelecemos. Neste caso, isso só vai baixar restaurantes onde category é "Dim Sum" .

Neste aplicativo, o usuário pode encadear vários filtros para criar consultas específicas, como "Pizza em San Francisco" ou "Frutos do mar em Los Angeles ordenados por popularidade".

Abrir RestaurantsTableViewController.swift e adicione o seguinte bloco de código para o meio da 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 fragmento acima adiciona múltiplos whereField e order cláusulas para construir uma única consulta composto com base na entrada do utilizador. Agora, nossa consulta retornará apenas restaurantes que atendam aos requisitos do usuário.

Execute seu projeto e verifique se você pode filtrar por preço, cidade e categoria (certifique-se de digitar os nomes da categoria e da cidade exatamente). Durante o teste, você pode ver erros em seus 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/testapp-5d356/database/firestore/indexes?create_index=..." 
UserInfo={NSLocalizedDescription=The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_index=...}

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. Abrindo o link da mensagem de erro irá abrir automaticamente a UI criação do índice no console Firebase com os parâmetros corretos preenchidos. Para saber mais sobre índices em Firestore, visite a documentação .

7. Gravando dados em uma transação

Nesta seção, adicionaremos a capacidade de os usuários enviarem avaliações para restaurantes. Até agora, todas as nossas gravações foram atômicas e relativamente simples. Se algum deles estivesse incorreto, provavelmente apenas solicitaríamos ao usuário que tentasse novamente ou tentasse novamente de forma automática.

Para adicionar uma classificação a um restaurante, precisamos coordenar várias leituras e gravações. Primeiro, a avaliação em si deve ser enviada e, em seguida, a contagem de classificação do restaurante e a classificação média devem ser atualizadas. Se um deles falhar, mas o outro não, ficamos em um estado inconsistente, em que os dados em uma parte do nosso banco de dados não correspondem aos dados em outra.

Felizmente, o Firestore fornece funcionalidade de transação que nos permite realizar várias leituras e gravações em uma única operação atômica, garantindo que nossos dados permaneçam consistentes.

Adicione o seguinte código abaixo todas as declarações deixar entrar 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)
    }
  }
}

Dentro do bloco de atualização, todas as operações que fazemos usando o objeto de transação serão tratadas como uma única atualização atômica pelo Firestore. Se a atualização falhar no servidor, o Firestore tentará novamente automaticamente algumas vezes. Isso significa que nossa condição de erro provavelmente é um único erro ocorrendo repetidamente, por exemplo, se o dispositivo estiver completamente offline ou se o usuário não estiver autorizado a gravar no caminho que está tentando gravar.

8. Regras de segurança

Os usuários de nosso aplicativo não devem ser capazes de ler e gravar todos os dados em nosso banco de dados. Por exemplo, todos devem ser capazes de ver as classificações de um restaurante, mas apenas um usuário autenticado deve ter permissão para postar uma classificação. Não é suficiente escrever um bom código no cliente, precisamos especificar nosso modelo de segurança de dados no back-end para ser totalmente seguro. Nesta seção, aprenderemos como usar as regras de segurança do Firebase para proteger nossos dados.

Primeiro, vamos dar uma olhada mais profunda nas regras de segurança que escrevemos no início do codelab. Abra o console Firebase e navegue para banco de dados> Regras no separador 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;
    }
  }
}

O request variável nas regras acima é uma variável global disponível em todas as regras, e nós condicional garante que o pedido é autenticado antes de permitir que os usuários fazer nada acrescentou. Isso evita que usuários não autenticados usem a API do Firestore para fazer alterações não autorizadas em seus dados. Este é um bom começo, mas podemos usar as regras do Firestore para fazer coisas muito mais poderosas.

Vamos restringir as gravações de revisão para que o ID do usuário da revisão corresponda ao ID do usuário autenticado. Isso garante que os usuários não possam se passar por outros e deixar comentários fraudulentos. Substitua suas regras de segurança pelo seguinte:

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 declaração fósforo fósforos os subcoleção chamado ratings de qualquer documento pertencente ao restaurants coleção. O allow write condicional, em seguida, impede que qualquer comentário de ser submetido se da revisão ID de usuário não coincide com a 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 análises, já que usamos regras de segurança para declarar explicitamente a garantia implícita que incluímos em nosso aplicativo anteriormente - que os usuários só podem escrever suas próprias análises. Se tivéssemos que adicionar uma função de edição ou exclusão para revisões, esse mesmo conjunto de regras também impediria os usuários de modificar ou excluir as revisões de outros usuários. Mas as regras do Firestore também podem ser usadas de maneira mais granular para limitar as gravações em campos individuais dentro dos documentos, em vez de nos próprios documentos inteiros. Podemos usar isso para permitir que os usuários atualizem apenas as classificações, a classificação média e o número de classificações de um restaurante, removendo a possibilidade de um usuário mal-intencionado alterar o nome ou localização 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 nossa permissão de gravação em criar e atualizar para que possamos ser mais específicos 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, mas uma vez que um restaurante é gravado, seu nome, localização, preço e categoria não podem ser alterados. Mais especificamente, a última regra requer 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 você pode fazer com as regras de segurança, dê uma olhada na documentação .

9. Conclusão

Neste codelab, você aprendeu como fazer leituras e gravações básicas e avançadas com o Firestore, além de como proteger o acesso a dados com regras de segurança. Você pode encontrar a solução completa no codelab-complete ramo .

Para saber mais sobre o Firestore, visite os seguintes recursos: