Laboratorium programowania w Cloud Firestore iOS

Cele

Podczas tego ćwiczenia z kodowania zbudujesz wspieraną przez Firestore aplikację do polecania restauracji na iOS w Swift. Dowiesz się jak:

  1. Odczytuj i zapisuj dane w Firestore z aplikacji na iOS
  2. Słuchaj zmian w danych Firestore w czasie rzeczywistym
  3. Użyj uwierzytelniania Firebase i reguł bezpieczeństwa, aby zabezpieczyć dane Firestore
  4. Pisz złożone zapytania Firestore

Warunki wstępne

Przed rozpoczęciem tego ćwiczenia z programowania upewnij się, że zainstalowałeś:

  • Xcode w wersji 8.3 (lub nowszej)
  • CocoaPods 1.2.1 (lub nowszy)

Dodaj Firebase do projektu

  1. Przejdź do konsoli Firebase .
  2. Wybierz Utwórz nowy projekt i nazwij swój projekt „Firestore iOS Codelab”.

Pobierz kod

Zacznij od sklonowania przykładowego projektu i uruchomienia pod update w katalogu projektu:

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

Otwórz FriendlyEats.xcworkspace w Xcode i uruchom go (Cmd+R). Aplikacja powinna się poprawnie skompilować i natychmiast zawiesić się po uruchomieniu, ponieważ brakuje w GoogleService-Info.plist pliku GoogleService-Info.plist . Poprawimy to w następnym kroku.

Skonfiguruj Firebase

Postępuj zgodnie z dokumentacją, aby utworzyć nowy projekt Firestore. Gdy masz swój projekt, Pobierz projektu GoogleService-Info.plist plik z Firebase konsoli i przeciągnij go do katalogu głównego projektu Xcode. Uruchom projekt ponownie, aby upewnić się, że aplikacja jest poprawnie skonfigurowana i nie ulega już awarii podczas uruchamiania. Po zalogowaniu powinieneś zobaczyć pusty ekran, jak na poniższym przykładzie. Jeśli nie możesz się zalogować, upewnij się, że w sekcji Uwierzytelnianie włączono metodę logowania przez e-mail/hasło w konsoli Firebase.

10a0671ce8f99704.png

W tej sekcji zapiszemy trochę danych do Firestore, abyśmy mogli wypełnić interfejs aplikacji. Można to zrobić ręcznie za pomocą konsoli Firebase , ale zrobimy to w samej aplikacji, aby zademonstrować podstawowy zapis w Firestore.

Głównym obiektem modelowym w naszej aplikacji jest restauracja. Dane Firestore są podzielone na dokumenty, kolekcje i podkolekcje. Każda restauracja będzie przechowywana jako dokument w zbiorze najwyższego poziomu o nazwie restaurants . Jeśli chcesz dowiedzieć się więcej o modelu danych Firestore, przeczytaj o dokumentach i kolekcjach w dokumentacji .

Zanim będziemy mogli dodać dane do Firestore, musimy uzyskać odniesienie do kolekcji restauracji. Dodaj następujące elementy do wewnętrznej pętli for w metodzie RestaurantsTableViewController.didTapPopulateButton(_:) .

0b977777990

Teraz, gdy mamy odniesienie do kolekcji, możemy zapisać pewne dane. Dodaj następujące tuż po ostatnim wierszu kodu, który dodaliśmy:

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)

Powyższy kod dodaje nowy dokument do kolekcji restauracji. Dane dokumentu pochodzą ze słownika, który otrzymujemy ze struktury Restaurant.

Jesteśmy prawie na miejscu – zanim będziemy mogli pisać dokumenty do Firestore, musimy otworzyć reguły bezpieczeństwa Firestore i opisać, które części naszej bazy danych powinny być zapisywalne dla których użytkowników. Na razie zezwalamy tylko uwierzytelnionym użytkownikom na odczytywanie i zapisywanie całej bazy danych. Jest to trochę zbyt liberalne w przypadku aplikacji produkcyjnej, ale podczas procesu tworzenia aplikacji chcemy, aby coś było wystarczająco rozluźnione, aby podczas eksperymentowania nie natrafiać na problemy z uwierzytelnianiem. Na końcu tego ćwiczenia z kodowania porozmawiamy o tym, jak wzmocnić reguły bezpieczeństwa i ograniczyć możliwość niezamierzonych odczytów i zapisów.

Na karcie Reguły konsoli Firebase dodaj następujące reguły, a następnie kliknij Publikuj .

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

Reguły bezpieczeństwa omówimy szczegółowo później, ale jeśli się spieszysz, zajrzyj do dokumentacji reguł bezpieczeństwa .

Uruchom aplikację i zaloguj się. Następnie dotknij przycisku „ Wypełnij ” w lewym górnym rogu, co spowoduje utworzenie partii dokumentów restauracji, chociaż nie zobaczysz tego jeszcze w aplikacji.

Następnie przejdź do karty danych Firestore w konsoli Firebase. Powinieneś zobaczyć nowe wpisy w kolekcji restauracji:

Zrzut ekranu 06.07.2017 o godzinie 12.45.38.png

Gratulacje, właśnie zapisałeś dane do Firestore z aplikacji na iOS! W następnej sekcji dowiesz się, jak pobierać dane z Firestore i wyświetlać je w aplikacji.

W tej sekcji dowiesz się, jak pobierać dane z Firestore i wyświetlać je w aplikacji. Dwa kluczowe kroki to utworzenie zapytania i dodanie detektora migawki. Ten detektor zostanie powiadomiony o wszystkich istniejących danych, które pasują do zapytania i będzie otrzymywać aktualizacje w czasie rzeczywistym.

Najpierw skonstruujmy zapytanie, które będzie obsługiwać domyślną, niefiltrowaną listę restauracji. Spójrz na implementację RestaurantsTableViewController.baseQuery() :

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

To zapytanie pobiera maksymalnie 50 restauracji z kolekcji najwyższego poziomu o nazwie „restauracje”. Teraz, gdy mamy zapytanie, musimy dołączyć detektor migawek, aby załadować dane z Firestore do naszej aplikacji. Dodaj następujący kod do metody RestaurantsTableViewController.observeQuery() tuż po wywołaniu stopObserving() .

07w93d270

Powyższy kod pobiera kolekcję z Firestore i przechowuje ją lokalnie w tablicy. addSnapshotListener(_:) dodaje do zapytania detektor migawek, który aktualizuje kontroler widoku za każdym razem, gdy dane na serwerze ulegną zmianie. Otrzymujemy aktualizacje automatycznie i nie musimy ręcznie wprowadzać zmian. Pamiętaj, że ten detektor migawek może zostać wywołany w dowolnym momencie w wyniku zmiany po stronie serwera, dlatego ważne jest, aby nasza aplikacja mogła obsłużyć zmiany.

Po zmapowaniu naszych słowników do struktur (patrz Restaurant.swift ) wyświetlenie danych to tylko kwestia przypisania kilku właściwości widoku. Dodaj następujące wiersze do RestaurantTableViewCell.populate(restaurant:) w 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)

Ta metoda wypełniania jest wywoływana z metody tableView(_:cellForRowAtIndexPath:) źródła danych widoku tabeli, która zajmuje się mapowaniem kolekcji typów wartości sprzed do poszczególnych komórek widoku tabeli.

Uruchom aplikację ponownie i sprawdź, czy restauracje, które widzieliśmy wcześniej w konsoli, są teraz widoczne na symulatorze lub urządzeniu. Jeśli pomyślnie ukończyłeś tę sekcję, Twoja aplikacja odczytuje i zapisuje dane w Cloud Firestore!

2ca7f8c6052f7f79.png

Obecnie nasza aplikacja wyświetla listę restauracji, ale użytkownik nie ma możliwości filtrowania według swoich potrzeb. W tej sekcji użyjesz zaawansowanych zapytań Firestore, aby włączyć filtrowanie.

Oto przykład prostego zapytania do pobrania wszystkich restauracji Dim Sum:

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

Jak sama nazwa wskazuje, whereField(_:isEqualTo:) spowoduje, że nasze zapytanie pobierze tylko elementy kolekcji, których pola spełniają określone przez nas ograniczenia. W tym przypadku pobierze tylko te restauracje, których category to "Dim Sum" .

W tej aplikacji użytkownik może połączyć wiele filtrów, aby utworzyć konkretne zapytania, takie jak „Pizza w San Francisco” lub „Owoce morza w Los Angeles uporządkowane według popularności”.

Otwórz RestaurantsTableViewController.swift i dodaj następujący blok kodu w środku 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)
}

Powyższy fragment whereField dodaje wiele klauzul whereField i order celu utworzenia pojedynczego zapytania złożonego na podstawie danych wprowadzonych przez użytkownika. Teraz nasze zapytanie zwróci tylko te restauracje, które spełniają wymagania użytkownika.

Uruchom swój projekt i sprawdź, czy możesz filtrować według ceny, miasta i kategorii (upewnij się, że wpisujesz dokładnie nazwę kategorii i miasta). Podczas testowania możesz zobaczyć błędy w swoich logach, które wyglądają tak:

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

Dzieje się tak, ponieważ Firestore wymaga indeksów dla większości zapytań złożonych. Wymaganie indeksów w zapytaniach sprawia, że ​​Firestore działa szybko na dużą skalę. Otwarcie linku z komunikatu o błędzie spowoduje automatyczne otwarcie interfejsu tworzenia indeksu w konsoli Firebase z wypełnionymi poprawnymi parametrami. Więcej informacji o indeksach w Firestore znajdziesz w dokumentacji .

W tej sekcji dodamy możliwość przesyłania opinii do restauracji przez użytkowników. Jak dotąd wszystkie nasze pisma były atomowe i stosunkowo proste. Jeśli któryś z nich jest błędny, prawdopodobnie po prostu poprosimy użytkownika o ponowną próbę lub ponowną próbę automatycznie.

Aby dodać ocenę do restauracji, musimy skoordynować wiele odczytów i zapisów. Najpierw należy przesłać samą recenzję, a następnie zaktualizować liczbę ocen restauracji i średnią ocenę. Jeśli jeden z nich zawiedzie, ale nie drugi, pozostaniemy w niespójnym stanie, w którym dane w jednej części naszej bazy danych nie pasują do danych w innej.

Na szczęście Firestore zapewnia funkcjonalność transakcji, która pozwala nam wykonywać wiele odczytów i zapisów w jednej atomowej operacji, zapewniając spójność naszych danych.

Dodaj następujący kod poniżej wszystkich deklaracji let w 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)
    }
  }
}

Wewnątrz bloku aktualizacji wszystkie operacje, które wykonujemy przy użyciu obiektu transakcji, będą traktowane przez Firestore jako pojedyncza aktualizacja atomowa. Jeśli aktualizacja nie powiedzie się na serwerze, Firestore automatycznie ponowi ją kilka razy. Oznacza to, że nasz stan błędu jest najprawdopodobniej pojedynczym błędem występującym wielokrotnie, na przykład, gdy urządzenie jest całkowicie offline lub użytkownik nie jest upoważniony do zapisu na ścieżce, na którą próbuje pisać.

Użytkownicy naszej aplikacji nie powinni mieć możliwości odczytywania i zapisywania wszystkich danych w naszej bazie danych. Na przykład każdy powinien być w stanie zobaczyć oceny restauracji, ale tylko uwierzytelniony użytkownik powinien mieć możliwość opublikowania oceny. Nie wystarczy napisać dobry kod na kliencie, musimy określić nasz model bezpieczeństwa danych na backendzie, aby był całkowicie bezpieczny. W tej sekcji dowiemy się, jak używać reguł bezpieczeństwa Firebase do ochrony naszych danych.

Najpierw przyjrzyjmy się bliżej regułom bezpieczeństwa, które napisaliśmy na początku ćwiczenia z kodowania. Otwórz konsolę Firebase i przejdź do Baza danych > Reguły na karcie 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;
    }
  }
}

Zmienna request w powyższych regułach jest zmienną globalną dostępną we wszystkich regułach, a dodany przez nas warunek warunkowy zapewnia uwierzytelnienie żądania przed zezwoleniem użytkownikom na zrobienie czegokolwiek. Uniemożliwia to nieuwierzytelnionym użytkownikom używanie interfejsu Firestore API do wprowadzania nieautoryzowanych zmian w Twoich danych. To dobry początek, ale możemy użyć reguł Firestore do znacznie potężniejszych rzeczy.

Ograniczmy zapisy recenzji, aby identyfikator użytkownika recenzji był zgodny z identyfikatorem uwierzytelnionego użytkownika. Gwarantuje to, że użytkownicy nie mogą podszywać się pod siebie i zostawiać nieprawdziwe recenzje. Zastąp swoje reguły bezpieczeństwa następującymi:

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

Pierwsze stwierdzenie dopasowania pasuje do podkolekcji nazwanych ratings dowolnego dokumentu należącego do kolekcji restaurants . Warunek allow write uniemożliwia następnie przesłanie recenzji, jeśli identyfikator użytkownika recenzji nie jest zgodny z identyfikatorem użytkownika. Druga instrukcja dopasowania umożliwia każdemu uwierzytelnionemu użytkownikowi odczytywanie i zapisywanie restauracji w bazie danych.

Działa to naprawdę dobrze w przypadku naszych recenzji, ponieważ użyliśmy reguł bezpieczeństwa, aby wyraźnie określić niejawną gwarancję, którą wcześniej napisaliśmy w naszej aplikacji — że użytkownicy mogą pisać tylko własne recenzje. Gdybyśmy mieli dodać funkcję edycji lub usunięcia recenzji, ten sam zestaw reguł uniemożliwiłby również użytkownikom modyfikowanie lub usuwanie opinii innych użytkowników. Ale reguły Firestore mogą być również używane w bardziej szczegółowy sposób, aby ograniczyć zapisy w poszczególnych polach w dokumentach, a nie w całych dokumentach. Możemy to wykorzystać, aby umożliwić użytkownikom aktualizowanie tylko ocen, średniej oceny i liczby ocen restauracji, eliminując możliwość zmiany nazwy lub lokalizacji restauracji przez złośliwego użytkownika.

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

Tutaj podzieliliśmy nasze uprawnienia do zapisu na tworzenie i aktualizowanie, dzięki czemu możemy dokładniej określić, które operacje powinny być dozwolone. Każdy użytkownik może zapisywać restauracje w bazie danych, zachowując funkcjonalność przycisku Wypełnij, który stworzyliśmy na początku ćwiczenia z kodowania, ale po zapisaniu restauracji jej nazwy, lokalizacji, ceny i kategorii nie można zmienić. Dokładniej, ostatnia reguła wymaga, aby każda operacja aktualizacji restauracji zachowała tę samą nazwę, miasto, cenę i kategorię już istniejących pól w bazie danych.

Aby dowiedzieć się więcej o tym, co możesz zrobić z regułami bezpieczeństwa, zapoznaj się z dokumentacją .

Podczas tego ćwiczenia kodowania nauczyłeś się, jak wykonywać podstawowe i zaawansowane odczyty i zapisy za pomocą Firestore, a także jak zabezpieczyć dostęp do danych za pomocą reguł bezpieczeństwa. Pełne rozwiązanie można znaleźć w gałęzi codelab-complete .

Aby dowiedzieć się więcej o Firestore, odwiedź następujące zasoby: