Cloud Firestore iOS Codelab

1. Обзор

Цели

В этой кодовой лаборатории вы создадите приложение с рекомендациями ресторанов на базе Firestore на iOS на Swift. Вы научитесь:

  1. Чтение и запись данных в Firestore из приложения iOS
  2. Слушайте изменения в данных Firestore в реальном времени
  3. Используйте Firebase Authentication и правила безопасности для защиты данных Firestore
  4. Написание сложных запросов Firestore

Предпосылки

Перед запуском этой кодовой лаборатории убедитесь, что вы установили:

  • Xcode версии 8.3 (или выше)
  • CocoaPods 1.2.1 (и выше)

2. Создайте консольный проект Firebase.

Добавить Firebase в проект

  1. Перейти к консоли Firebase .
  2. Выберите Создать новый проект и назовите проект «Firestore IOS Codelab».

3. Получите образец проекта.

Скачать код

Начните клонировать пример проект и запуск pod update в каталоге проекта:

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

Открыть FriendlyEats.xcworkspace в Xcode и запустить его (Ctrl + R). Приложение должно составить правильно и сразу же аварии на старте, так как он отсутствует GoogleService-Info.plist файл. Мы исправим это на следующем шаге.

Настроить Firebase

Следуйте документации для создания нового проекта Firestore. После того, как вы получили ваш проект, загрузите ваш проект GoogleService-Info.plist файл из Firebase консоли и перетащить его в корень проекта Xcode. Снова запустите проект, чтобы убедиться, что приложение настроено правильно и больше не дает сбоев при запуске. После входа в систему вы должны увидеть пустой экран, как в примере ниже. Если вам не удается войти в систему, убедитесь, что вы включили метод входа по электронной почте / паролю в консоли Firebase в разделе «Аутентификация».

10a0671ce8f99704.png

4. Запись данных в Firestore.

В этом разделе мы напишем некоторые данные в Firestore, чтобы мы могли заполнить пользовательский интерфейс приложения. Это можно сделать вручную с помощью Firebase консоли , но мы будем делать это в самом приложении , чтобы продемонстрировать основные Firestore записи.

Основным объектом модели в нашем приложении является ресторан. Данные Firestore делятся на документы, коллекции и вложенные коллекции. Мы будем хранить каждый ресторан в качестве документа в коллекции верхнего уровня называется restaurants . Если вы хотите узнать больше о модели данных Firestore, читать о документах и коллекциях в документации .

Прежде чем мы сможем добавить данные в Firestore, нам нужно получить ссылку на коллекцию ресторанов. Добавьте следующее к внутренней цикл в RestaurantsTableViewController.didTapPopulateButton(_:) метода.

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

Теперь, когда у нас есть ссылка на коллекцию, мы можем записать некоторые данные. Добавьте следующее сразу после последней строки кода, которую мы добавили:

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)

Приведенный выше код добавляет новый документ в коллекцию ресторанов. Данные документа берутся из словаря, который мы получаем из структуры Restaurant.

Мы почти у цели - прежде чем мы сможем писать документы в Firestore, нам нужно открыть правила безопасности Firestore и описать, какие части нашей базы данных должны быть доступны для записи какими пользователями. На данный момент мы разрешим только аутентифицированным пользователям читать и писать во всей базе данных. Это немного слишком разрешительно для производственного приложения, но во время процесса создания приложения нам нужно что-то достаточно расслабленное, чтобы мы не сталкивались постоянно с проблемами аутентификации во время экспериментов. В конце этой кодовой лаборатории мы поговорим о том, как усилить ваши правила безопасности и ограничить возможность непреднамеренного чтения и записи.

В закладке Правила консоли Firebase добавить следующие правила , а затем нажмите кнопку Опубликовать.

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

Мы обсудим правила безопасности подробно позже, но если вы в спешке, взгляните на документацию правил безопасности .

Запустите приложение и войти. Затем нажмите кнопку «Заполнить» в верхнем левом углу, что создаст партию ресторана документов, хотя вы не будете видеть это в приложении еще.

Затем перейдите к закладке данных Firestore в консоли Firebase. Теперь вы должны увидеть новые записи в коллекции ресторанов:

Снимок экрана, 6 июля 2017 г., 12: 45.38.png

Поздравляем, вы только что записали данные в Firestore из приложения для iOS! В следующем разделе вы узнаете, как получить данные из Firestore и отобразить их в приложении.

5. Отображение данных из Firestore

В этом разделе вы узнаете, как получить данные из Firestore и отобразить их в приложении. Два ключевых шага - это создание запроса и добавление прослушивателя моментальных снимков. Этот слушатель будет уведомлен обо всех существующих данных, соответствующих запросу, и будет получать обновления в режиме реального времени.

Во-первых, давайте создадим запрос, который будет обслуживать нефильтрованный список ресторанов по умолчанию. Посмотрите на реализацию RestaurantsTableViewController.baseQuery() :

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

Этот запрос извлекает до 50 ресторанов из коллекции верхнего уровня с именем "Restaurants". Теперь, когда у нас есть запрос, нам нужно подключить прослушиватель моментальных снимков для загрузки данных из Firestore в наше приложение. Добавьте следующий код в RestaurantsTableViewController.observeQuery() метод только после вызова 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()
}

Приведенный выше код загружает коллекцию из Firestore и сохраняет ее в массиве локально. addSnapshotListener(_:) вызов добавляет снимок слушателя на запрос , который будет обновлять контроллер представления каждый раз , когда данные изменения на сервере. Мы получаем обновления автоматически, и нам не нужно вносить изменения вручную. Помните, что этот прослушиватель моментальных снимков может быть вызван в любое время в результате изменения на стороне сервера, поэтому важно, чтобы наше приложение могло обрабатывать изменения.

После сопоставления наших словарей структур (см Restaurant.swift ), отображение данных является лишь вопросом назначения нескольких свойств вида. Добавьте следующие строки в RestaurantTableViewCell.populate(restaurant:) в 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)

Этот Заселите метод вызывается из источника данных таблицы tableView(_:cellForRowAtIndexPath:) метод, который берет на себя отображение коллекции типов значений от до того, чтобы просмотреть отдельные ячейки таблицы.

Снова запустите приложение и убедитесь, что рестораны, которые мы видели ранее в консоли, теперь видны на симуляторе или устройстве. Если вы успешно завершили этот раздел, ваше приложение теперь читает и записывает данные с помощью Cloud Firestore!

2ca7f8c6052f7f79.png

6. Сортировка и фильтрация данных.

В настоящее время наше приложение отображает список ресторанов, но пользователь не может фильтровать его по своим потребностям. В этом разделе вы будете использовать расширенные запросы Firestore для включения фильтрации.

Вот пример простого запроса для получения всех ресторанов дим-сам:

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

Как следует из названия, whereField(_:isEqualTo:) метод сделает наш запрос скачать только член коллекции , чьи поля удовлетворяют ограничения , которые мы поставили. В этом случае, это будет только скачать рестораны , где category является "Dim Sum" .

В этом приложении пользователь может связать несколько фильтров для создания определенных запросов, например «Пицца в Сан-Франциско» или «Морепродукты в Лос-Анджелесе, заказанные по популярности».

Открыть RestaurantsTableViewController.swift и добавьте следующий блок кода в середине 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)
}

Выше фрагмент кода добавляет несколько whereField и order положения для создания единого составного запроса на основе пользовательского ввода. Теперь наш запрос будет возвращать только те рестораны, которые соответствуют требованиям пользователя.

Запустите свой проект и убедитесь, что вы можете фильтровать по цене, городу и категории (обязательно введите категорию и названия городов). Во время тестирования вы можете увидеть ошибки в своих журналах, которые выглядят следующим образом:

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=...}

Это связано с тем, что Firestore требует индексов для большинства составных запросов. Требование индексов для запросов позволяет Firestore быстро масштабироваться. Открытие ссылки из сообщения об ошибке автоматически откроется интерфейс создания индекса в консоли Firebase с правильными параметрами заполненных. Чтобы узнать о более индексах в Firestore, посетите документацию .

7. Запись данных в транзакцию

В этом разделе мы добавим возможность пользователям отправлять отзывы о ресторанах. До сих пор все наши записи были атомарными и относительно простыми. Если в каком-либо из них возникнет ошибка, мы, скорее всего, просто предложим пользователю повторить их или повторить попытку автоматически.

Чтобы добавить ресторану рейтинг, нам нужно скоординировать несколько операций чтения и записи. Сначала необходимо отправить сам отзыв, а затем обновить счетчик оценок и средний рейтинг ресторана. Если один из них не работает, а другой нет, мы остаемся в несогласованном состоянии, когда данные в одной части нашей базы данных не совпадают с данными в другой.

К счастью, Firestore предоставляет функциональные возможности транзакций, которые позволяют нам выполнять несколько операций чтения и записи за одну атомарную операцию, гарантируя, что наши данные остаются согласованными.

Добавьте следующий код ниже всех объявлений впустить 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)
    }
  }
}

Внутри блока обновления все операции, которые мы выполняем с использованием объекта транзакции, будут обрабатываться Firestore как одно атомарное обновление. Если обновление не выполняется на сервере, Firestore автоматически повторяет его несколько раз. Это означает, что наше состояние ошибки, скорее всего, представляет собой однократную ошибку, повторяющуюся неоднократно, например, если устройство полностью отключено или пользователь не авторизован для записи по пути, по которому он пытается писать.

8. Правила безопасности

Пользователи нашего приложения не должны иметь возможность читать и записывать все данные в нашей базе данных. Например, каждый должен иметь возможность видеть рейтинги ресторана, но только аутентифицированный пользователь должен иметь право публиковать рейтинг. Недостаточно написать хороший код на клиенте, нам нужно указать нашу модель безопасности данных на бэкэнде, чтобы она была полностью защищенной. В этом разделе мы узнаем, как использовать правила безопасности Firebase для защиты наших данных.

Во-первых, давайте более подробно рассмотрим правила безопасности, которые мы написали в начале кодовой лаборатории. Откройте консоль Firebase и перейдите к базе данных> Правила на вкладке 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;
    }
  }
}

request переменный в правилах выше , является глобальным переменным во всех правилах, и условные мы добавили гарантируем , что запрос с проверкой подлинности , прежде чем разрешить пользователям делать что - нибудь. Это предотвращает использование неаутентифицированными пользователями API Firestore для внесения несанкционированных изменений в ваши данные. Это хорошее начало, но мы можем использовать правила Firestore, чтобы делать гораздо более мощные вещи.

Давайте ограничим запись для проверки так, чтобы идентификатор пользователя для проверки совпадал с идентификатором аутентифицированного пользователя. Это гарантирует, что пользователи не смогут выдавать себя за друг друга и оставлять мошеннические отзывы. Замените свои правила безопасности следующими:

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

Первый оператор матча соответствует субколлекциям имени ratings любого документа , принадлежащему restaurants коллекции. allow write условные затем предотвращает обзор от их представлений , если обзор идентификатор пользователя не совпадает пользователь. Второй оператор соответствия позволяет любому аутентифицированному пользователю читать и записывать рестораны в базу данных.

Это действительно хорошо работает для наших обзоров, поскольку мы использовали правила безопасности, чтобы явно указать неявную гарантию, которую мы ранее ввели в наше приложение, - что пользователи могут писать только свои собственные обзоры. Если бы мы добавили функцию редактирования или удаления для обзоров, тот же самый набор правил также не позволял бы пользователям изменять или удалять отзывы других пользователей. Но правила Firestore можно также использовать более детально, чтобы ограничить запись в отдельные поля в документах, а не для самих документов. Мы можем использовать это, чтобы позволить пользователям обновлять только рейтинги, средний рейтинг и количество оценок для ресторана, исключая возможность злонамеренного изменения названия или местоположения ресторана.

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

Здесь мы разделили наше разрешение на запись на создание и обновление, чтобы мы могли более точно определить, какие операции следует разрешить. Любой пользователь может записывать рестораны в базу данных, сохраняя функциональность кнопки «Заполнить», которую мы создали в начале кодовой таблицы, но после того, как ресторан записан, его название, местоположение, цена и категория не могут быть изменены. В частности, последнее правило требует, чтобы любая операция обновления ресторана поддерживала те же имя, город, цену и категорию, что и в уже существующих полях в базе данных.

Чтобы узнать больше о том, что вы можете сделать с правилами безопасности, обратите внимание на документацию .

9. Заключение

В этой кодовой лаборатории вы узнали, как выполнять базовые и расширенные операции чтения и записи с помощью Firestore, а также как защитить доступ к данным с помощью правил безопасности. Вы можете найти полное решение на codelab-complete ветви .

Чтобы узнать больше о Firestore, посетите следующие ресурсы: