Облако Firestore iOS Codelab

Оптимизируйте свои подборки Сохраняйте и классифицируйте контент в соответствии со своими настройками.

1. Обзор

Цели

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

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

Предпосылки

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

  • Xcode версии 13.0 (или выше)
  • CocoaPods 1.11.0 (или выше)

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 и запустите его (Cmd+R). Приложение должно правильно скомпилироваться и сразу же вылететь при запуске, так как в нем отсутствует файл GoogleService-Info.plist . Мы исправим это на следующем шаге.

Настроить Firebase

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

d5225270159c040b.png

4. Запишите данные в Firestore

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

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

Прежде чем мы сможем добавить данные в Firestore, нам нужно получить ссылку на коллекцию ресторанов. Добавьте следующее во внутренний цикл for в методе 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. Теперь вы должны увидеть новые записи в коллекции ресторанов:

Скриншот 06.07.2017, 12.45.38.png

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

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

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

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

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

Этот запрос извлекает до 50 ресторанов из коллекции верхнего уровня с именем "рестораны". Теперь, когда у нас есть запрос, нам нужно подключить прослушиватель моментальных снимков для загрузки данных из 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!

391c0259bf05ac25.png

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

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

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

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 предоставляет функциональные возможности транзакций, которые позволяют нам выполнять несколько операций чтения и записи в одной атомарной операции, гарантируя, что наши данные останутся согласованными.

Добавьте следующий код под всеми объявлениями let в 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 в приведенных выше правилах — это глобальная переменная, доступная во всех правилах, а добавленное нами условие гарантирует, что запрос аутентифицирован, прежде чем пользователи смогут что-либо сделать. Это предотвращает использование Firestore API неавторизованными пользователями для внесения несанкционированных изменений в ваши данные. Это хорошее начало, но мы можем использовать правила 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, посетите следующие ресурсы: