Google is committed to advancing racial equity for Black communities. See how.
Эта страница переведена с помощью Cloud Translation API.
Switch to English

Cloud Firestore iOS Codelab

Цели

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

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

Предпосылки

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

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

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

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

Скачать код

Начните с клонирования образца проекта и выполнения pod update в каталоге проекта:

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

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

Настроить Firebase

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

10a0671ce8f99704.png

В этом разделе мы напишем некоторые данные в 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. Теперь вы должны увидеть новые записи в коллекции ресторанов:

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

Поздравляем, вы только что записали данные в Firestore из приложения для iOS! В следующем разделе вы узнаете, как получить данные из 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:) источника данных представления таблицы, который заботится о сопоставлении коллекции типов значений из tableView(_:cellForRowAtIndexPath:) вида с отдельными ячейками представления таблицы.

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

2ca7f8c6052f7f79.png

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

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

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

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

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

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

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

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

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