1. Przegląd
Cele
W tym laboratorium kodowym zbudujesz wspieraną przez Firestore aplikację rekomendacji restauracji na iOS w Swift. Dowiesz się, jak:
- Odczytuj i zapisuj dane w Firestore z aplikacji na iOS
- Słuchaj zmian w danych Firestore w czasie rzeczywistym
- Użyj uwierzytelniania Firebase i reguł bezpieczeństwa, aby zabezpieczyć dane Firestore
- Pisz złożone zapytania Firestore
Wymagania wstępne
Przed rozpoczęciem tego laboratorium kodowania upewnij się, że masz zainstalowane:
- Wersja Xcode 14.0 (lub nowsza)
- CocoaPods 1.12.0 (lub nowszy)
2. Utwórz projekt konsoli Firebase
Dodaj Firebase do projektu
- Przejdź do konsoli Firebase .
- Wybierz opcję Utwórz nowy projekt i nazwij swój projekt „Firestore iOS Codelab”.
3. Pobierz przykładowy projekt
Pobierz kod
Rozpocznij 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 skompilować się poprawnie i natychmiast po uruchomieniu ulec awarii, ponieważ brakuje w niej pliku GoogleService-Info.plist
. Poprawimy to w następnym kroku.
Skonfiguruj Firebase
Postępuj zgodnie z dokumentacją , aby utworzyć nowy projekt Firestore. Po utworzeniu projektu pobierz plik GoogleService-Info.plist
projektu z konsoli Firebase i przeciągnij go do katalogu głównego projektu Xcode. Ponownie uruchom projekt, aby upewnić się, że aplikacja konfiguruje się poprawnie i nie ulega już awariom 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łączyłeś metodę logowania przez e-mail/hasło w konsoli Firebase w sekcji Uwierzytelnianie.
4. Zapisz dane w Firestore
W tej sekcji zapiszemy niektóre dane 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 Firestore.
Głównym modelowym obiektem w naszej aplikacji jest restauracja. Dane Firestore są podzielone na dokumenty, kolekcje i kolekcje podrzędne. Każdą restaurację będziemy przechowywać jako dokument w kolekcji 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(_:)
.
let collection = Firestore.firestore().collection("restaurants")
Teraz, gdy mamy odwołanie do kolekcji, możemy zapisać trochę danych. Dodaj następujący tekst 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ć dostępne do zapisu dla poszczególnych użytkowników. Na razie zezwalamy tylko uwierzytelnionym użytkownikom na odczyt i zapis w całej bazie danych. Jest to trochę zbyt liberalne dla aplikacji produkcyjnej, ale podczas procesu tworzenia aplikacji chcemy czegoś na tyle swobodnego, aby podczas eksperymentowania nie napotykać ciągle problemów z uwierzytelnianiem. Na koniec tego laboratorium kodowania porozmawiamy o tym, jak zaostrzyć zasady bezpieczeństwa i ograniczyć możliwość niezamierzonego odczytu i zapisu.
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; } } }
Zasady bezpieczeństwa omówimy szczegółowo później, ale jeśli się spieszysz, zajrzyj do dokumentacji zasad bezpieczeństwa .
Uruchom aplikację i zaloguj się. Następnie naciśnij przycisk „ 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ś teraz zobaczyć nowe wpisy w kolekcji restauracji:
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.
5. Wyświetl dane z Firestore
W tej sekcji dowiesz się, jak pobierać dane z Firestore i wyświetlać je w aplikacji. Dwa kluczowe kroki to utworzenie zapytania i dodanie odbiornika migawek. Ten odbiornik zostanie powiadomiony o wszystkich istniejących danych pasujących do zapytania i otrzyma aktualizacje w czasie rzeczywistym.
Najpierw skonstruujmy zapytanie, które będzie wyświetlać 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ć odbiornik migawki, aby załadować dane z Firestore do naszej aplikacji. Dodaj następujący kod do metody RestaurantsTableViewController.observeQuery()
zaraz po wywołaniu metody 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()
}
Powyższy kod pobiera kolekcję z Firestore i przechowuje ją lokalnie w tablicy. Wywołanie addSnapshotListener(_:)
dodaje do zapytania odbiornik migawki, który będzie aktualizował kontroler widoku za każdym razem, gdy dane zmienią się na serwerze. Otrzymujemy aktualizacje automatycznie i nie musimy ręcznie wprowadzać zmian. Pamiętaj, że ten odbiornik migawki można wywołać 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 na struktury (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 z poprzedniego okresu do poszczególnych komórek widoku tabeli.
Ponownie uruchom aplikację 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!
6. Sortowanie i filtrowanie danych
Obecnie nasza aplikacja wyświetla listę restauracji, ale użytkownik nie ma możliwości filtrowania na podstawie swoich potrzeb. W tej sekcji użyjesz zaawansowanych zapytań Firestore, aby włączyć filtrowanie.
Oto przykład prostego zapytania, które pobierze wszystkie restauracje Dim Sum:
let filteredQuery = query.whereField("category", isEqualTo: "Dim Sum")
Jak sama nazwa wskazuje, metoda whereField(_:isEqualTo:)
spowoduje, że nasze zapytanie pobierze tylko członków kolekcji, których pola spełniają określone przez nas ograniczenia. W takim przypadku pobierze tylko restauracje, których category
to "Dim Sum"
.
W tej aplikacji użytkownik może połączyć wiele filtrów, aby utworzyć określone zapytania, takie jak „Pizza w San Francisco” lub „Owoce morza w Los Angeles zamówione 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 dodaje wiele klauzul whereField
i order
w celu zbudowania pojedynczego zapytania złożonego na podstawie danych wprowadzonych przez użytkownika. Teraz nasze zapytanie zwróci tylko restauracje spełniające wymagania użytkownika.
Uruchom projekt i sprawdź, czy możesz filtrować według ceny, miasta i kategorii (pamiętaj, aby dokładnie wpisać nazwę kategorii i miasta). Podczas testowania w dziennikach mogą pojawić się następujące błędy:
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/project-id/database/firestore/indexes?create_composite=..." UserInfo={NSLocalizedDescription=The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_composite=...}
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 i na dużą skalę. Otwarcie linku z komunikatu o błędzie spowoduje automatyczne otwarcie interfejsu tworzenia indeksu w konsoli Firebase z wypełnionymi poprawnymi parametrami. Aby dowiedzieć się więcej o indeksach w Firestore, odwiedź dokumentację .
7. Zapisywanie danych w transakcji
W tej sekcji dodamy możliwość przesyłania przez użytkowników recenzji do restauracji. Jak dotąd wszystkie nasze zapisy były atomowe i stosunkowo proste. Jeśli któryś z nich zawierał błąd, prawdopodobnie po prostu poprosilibyśmy użytkownika o ponowienie lub ponowienie próby 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 i średnią ocen restauracji. Jeśli jeden z nich zawiedzie, a drugi nie, pozostajemy 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ść transakcyjną, która pozwala nam wykonywać wiele odczytów i zapisów w jednej niepodzielnej 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 spróbuje kilka razy. Oznacza to, że nasz stan błędu to najprawdopodobniej powtarzający się pojedynczy błąd, na przykład jeśli urządzenie jest całkowicie offline lub użytkownik nie ma uprawnień do zapisu w ścieżce, do której próbuje pisać.
8. Zasady bezpieczeństwa
Użytkownicy naszej aplikacji nie powinni mieć możliwości odczytu i zapisu wszystkich danych w naszej bazie danych. Na przykład każdy powinien mieć możliwość przeglądania ocen restauracji, ale tylko uwierzytelniony użytkownik powinien mieć możliwość opublikowania oceny. Nie wystarczy napisać dobrego kodu na kliencie, musimy określić nasz model bezpieczeństwa danych na backendzie, aby były całkowicie bezpieczne. 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 laboratorium 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 warunek, który dodaliśmy, 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 danych. To dobry początek, ale możemy użyć reguł Firestore do znacznie potężniejszych rzeczy.
Ograniczmy pisanie recenzji, tak aby identyfikator użytkownika recenzji był zgodny z identyfikatorem uwierzytelnionego użytkownika. Dzięki temu użytkownicy nie będą mogli podszywać się pod siebie nawzajem i zostawiać fałszywych recenzji. 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;
}
}
}
Pierwsza instrukcja dopasowania pasuje do podzbioru nazwanych ratings
dowolnego dokumentu należącego do kolekcji restaurants
. allow write
zapobiega następnie przesłaniu jakiejkolwiek recenzji, jeśli identyfikator użytkownika recenzji nie jest zgodny z identyfikatorem użytkownika. Druga instrukcja dopasowania umożliwia dowolnemu uwierzytelnionemu użytkownikowi odczytywanie i zapisywanie restauracji w bazie danych.
Działa to bardzo dobrze w przypadku naszych recenzji, ponieważ zastosowaliśmy zasady bezpieczeństwa, aby wyraźnie określić dorozumianą gwarancję, którą zapisaliśmy wcześniej w naszej aplikacji — że użytkownicy mogą pisać tylko własne recenzje. Gdybyśmy dodali funkcję edytowania lub usuwania recenzji, dokładnie ten sam zestaw reguł uniemożliwiłby użytkownikom modyfikowanie lub usuwanie recenzji 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 samych 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, abyśmy mogli 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 zrobiliśmy na początku laboratorium kodów, ale po wpisaniu restauracji jej nazwy, lokalizacji, ceny i kategorii nie można zmienić. Mówiąc dokładniej, ostatnia reguła wymaga, aby każda operacja aktualizacji restauracji zachowywał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ą .
9. Wniosek
W tym laboratorium kodowania nauczyłeś się podstawowych i zaawansowanych odczytów i zapisów w 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: