1. Przegląd
Cele
W tym ćwiczeniu z programowania utworzysz w Swift aplikację na iOS do rekomendowania restauracji opartą na Firestore. Zapoznasz się z tymi zagadnieniami:
- Odczytywanie i zapisywanie danych w Firestore z aplikacji na iOS
- Nasłuchiwanie zmian w danych Firestore w czasie rzeczywistym
- Zabezpieczanie danych Firestore za pomocą uwierzytelniania Firebase i reguł zabezpieczeń
- Pisanie złożonych zapytań do Firestore
Wymagania wstępne
Zanim zaczniesz ten moduł, zainstaluj:
- Xcode w wersji 14.0 lub nowszej.
- CocoaPods w wersji 1.12.0 lub nowszej
2. Pobieranie przykładowego projektu
Pobieranie kodu
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ę prawidłowo skompilować i natychmiast ulec awarii po uruchomieniu, ponieważ brakuje w niej pliku GoogleService-Info.plist
. Poprawimy to w następnym kroku.
3. Konfigurowanie Firebase
Tworzenie projektu Firebase
- Zaloguj się w konsoli Firebase, korzystając ze swojego konta Google.
- Kliknij przycisk, aby utworzyć nowy projekt, a potem wpisz jego nazwę (np.
FriendlyEats
).
- Kliknij Dalej.
- Po wyświetleniu monitu przeczytaj i zaakceptuj warunki usługi Firebase, a potem kliknij Dalej.
- (Opcjonalnie) Włącz w konsoli Firebase pomoc AI (nazywaną „Gemini w Firebase”).
- W tym samouczku nie potrzebujesz Google Analytics, więc wyłącz opcję Google Analytics.
- Kliknij Utwórz projekt, poczekaj, aż projekt zostanie udostępniony, a następnie kliknij Dalej.
Łączenie aplikacji z Firebase
Utwórz aplikację na iOS w nowym projekcie Firebase.
Pobierz GoogleService-Info.plist
plik projektu z konsoli Firebase i przeciągnij go do katalogu głównego projektu Xcode. Uruchom projekt ponownie, aby sprawdzić, czy aplikacja jest prawidłowo skonfigurowana i nie ulega już awarii podczas uruchamiania. Po zalogowaniu powinien pojawić się pusty ekran, jak w przykładzie poniżej. Jeśli nie możesz się zalogować, sprawdź, czy w konsoli Firebase w sekcji Uwierzytelnianie masz włączoną metodę logowania za pomocą adresu e-mail i hasła.
4. Zapisywanie danych w Firestore
W tej sekcji zapiszemy w Firestore dane, aby wypełnić interfejs aplikacji. Możesz to zrobić ręcznie w konsoli Firebase, ale my zrobimy to w samej aplikacji, aby zademonstrować podstawowe zapisywanie w Firestore.
Głównym obiektem modelu w naszej aplikacji jest restauracja. Dane Firestore są podzielone na dokumenty, kolekcje i podkolekcje. Każda restauracja będzie przechowywana jako dokument w kolekcji najwyższego poziomu o nazwie restaurants
. Jeśli chcesz dowiedzieć się więcej o modelu danych Firestore, przeczytaj dokumentację dotyczącą dokumentów i kolekcji.
Zanim dodamy dane do Firestore, musimy uzyskać odniesienie do kolekcji restauracji. Dodaj do wewnętrznej pętli for w metodzie RestaurantsTableViewController.didTapPopulateButton(_:)
ten kod:
let collection = Firestore.firestore().collection("restaurants")
Mając odwołanie do kolekcji, możemy zapisać dane. Tuż za ostatnim dodanym wierszem kodu dodaj ten kod:
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 uzyskujemy ze struktury Restaurant.
Jesteśmy już prawie na miejscu – zanim będziemy mogli zapisywać dokumenty w Firestore, musimy otworzyć reguły zabezpieczeń Firestore i określić, które części naszej bazy danych powinny być zapisywalne przez których użytkowników. Obecnie zezwalamy tylko uwierzytelnionym użytkownikom na odczytywanie i zapisywanie danych w całej bazie danych. W przypadku aplikacji produkcyjnej jest to zbyt liberalne, ale podczas tworzenia aplikacji chcemy mieć coś wystarczająco elastycznego, aby podczas eksperymentowania nie napotykać ciągle problemów z uwierzytelnianiem. Na koniec tego laboratorium kodowania omówimy, jak wzmocnić reguły zabezpieczeń i ograniczyć możliwość niezamierzonego odczytu i zapisu.
Na karcie Reguły w konsoli Firebase dodaj te reguły, a potem kliknij Opublikuj.
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; } } }
Reguły zabezpieczeń omówimy szczegółowo w dalszej części, ale jeśli się spieszysz, zapoznaj się z dokumentacją reguł zabezpieczeń.
Uruchom aplikację i zaloguj się. Następnie w lewym górnym rogu kliknij przycisk „Wypełnij”, aby utworzyć partię dokumentów restauracji. Nie zobaczysz ich jeszcze w aplikacji.
Następnie w konsoli Firebase otwórz kartę danych Firestore. W kolekcji restauracji powinny pojawić się nowe wpisy:
Gratulacje! Właśnie zapisano dane w Firestore z aplikacji na iOS. W następnej sekcji dowiesz się, jak pobierać dane z Firestore i wyświetlać je w aplikacji.
5. Wyświetlanie danych z Firestore
Z tej sekcji dowiesz się, jak pobierać dane z Firestore i wyświetlać je w aplikacji. Dwa kluczowe kroki to utworzenie zapytania i dodanie detektora migawek. Ten odbiorca będzie otrzymywać powiadomienia o wszystkich istniejących danych pasujących do zapytania oraz 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 „restaurants”. Teraz, gdy mamy już zapytanie, musimy dołączyć do niego odbiornik migawek, aby wczytywać dane z Firestore do aplikacji. Dodaj ten kod do metody RestaurantsTableViewController.observeQuery()
tuż po wywołaniu 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 detektor zrzutu, który będzie aktualizować kontroler widoku za każdym razem, gdy dane na serwerze ulegną zmianie. Aktualizacje otrzymujemy automatycznie i nie musimy ręcznie wprowadzać zmian. Pamiętaj, że ten odbiornik migawek może zostać wywołany w dowolnym momencie w wyniku zmiany po stronie serwera, dlatego ważne jest, aby nasza aplikacja obsługiwała zmiany.
Po przypisaniu słowników do struktur (patrz Restaurant.swift
) wyświetlanie danych sprowadza się do przypisania kilku właściwości widoku. Dodaj te wiersze do pliku 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 wcześniejszego okresu na poszczególne komórki widoku tabeli.
Uruchom ponownie aplikację i sprawdź, czy restauracje, które były wcześniej widoczne w konsoli, są teraz widoczne w symulatorze lub na urządzeniu. Jeśli udało Ci się ukończyć 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 może filtrować wyników według swoich potrzeb. W tej sekcji użyjesz zaawansowanych zapytań Firestore, aby włączyć filtrowanie.
Oto przykład prostego zapytania o wszystkie restauracje serwujące dim sum:
let filteredQuery = query.whereField("category", isEqualTo: "Dim Sum")
Jak sama nazwa wskazuje, metoda whereField(_:isEqualTo:)
spowoduje, że zapytanie pobierze tylko elementy kolekcji, których pola spełniają ustawione przez nas ograniczenia. W tym przypadku pobierane będą tylko restauracje, w których category
ma wartość "Dim Sum"
.
W tej aplikacji użytkownik może łączyć ze sobą wiele filtrów, aby tworzyć konkretne zapytania, np. „Pizza w San Francisco” lub „Owoce morza w Los Angeles posortowane według popularności”.
Otwórz RestaurantsTableViewController.swift
i dodaj ten blok kodu na środku pliku 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 kodu dodaje wiele klauzul whereField
i order
, aby utworzyć jedno złożone zapytanie na podstawie danych wejściowych użytkownika. Teraz zapytanie będzie zwracać tylko restauracje, które spełniają wymagania użytkownika.
Uruchom projekt i sprawdź, czy możesz filtrować wyniki według ceny, miasta i kategorii (pamiętaj, aby wpisywać nazwy kategorii i miast dokładnie tak, jak są zapisane w bazie danych). Podczas testowania w dziennikach mogą pojawić się takie 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 w przypadku większości zapytań złożonych. Wymaganie indeksów w przypadku zapytań zapewnia dużą szybkość Firestore w przypadku dużej skali. Otwarcie linku z komunikatu o błędzie spowoduje automatyczne otwarcie interfejsu tworzenia indeksu w konsoli Firebase z wypełnionymi prawidłowymi parametrami. Więcej informacji o indeksach w Firestore znajdziesz w dokumentacji.
7. Zapisywanie danych w transakcji
W tej sekcji dodamy możliwość przesyłania opinii o restauracjach przez użytkowników. Do tej pory wszystkie nasze zapisy były niepodzielne i stosunkowo proste. Jeśli któryś z nich zakończy się błędem, prawdopodobnie poprosimy użytkownika o ponowienie próby lub automatycznie ponowimy próbę.
Aby dodać ocenę restauracji, musimy skoordynować wiele odczytów i zapisów. Najpierw musi zostać przesłana opinia, a potem trzeba zaktualizować liczbę ocen i średnią ocen restauracji. Jeśli jedna z tych operacji się nie powiedzie, a druga tak, powstanie niespójność, w której dane w jednej części bazy danych nie będą pasować do danych w innej części.
Na szczęście Firestore udostępnia funkcję transakcji, która umożliwia wykonywanie wielu odczytów i zapisów w ramach jednej operacji niepodzielnej, co zapewnia spójność danych.
Dodaj ten kod poniżej wszystkich deklaracji „let” w pliku 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)
}
}
}
W bloku aktualizacji wszystkie operacje wykonywane za pomocą obiektu transakcji będą traktowane przez Firestore jako jedna niepodzielna aktualizacja. Jeśli aktualizacja nie powiedzie się na serwerze, Firestore automatycznie ponowi próbę kilka razy. Oznacza to, że nasz warunek błędu jest najprawdopodobniej pojedynczym błędem występującym wielokrotnie, np. gdy urządzenie jest całkowicie offline lub użytkownik nie ma uprawnień do zapisu w ścieżce, w której próbuje zapisać dane.
8. Reguły zabezpieczeń
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 mieć możliwość wyświetlania ocen restauracji, ale tylko uwierzytelniony użytkownik powinien mieć możliwość opublikowania oceny. Samo napisanie dobrego kodu po stronie klienta nie wystarczy. Aby zapewnić pełne bezpieczeństwo, musimy określić model bezpieczeństwa danych na backendzie. W tej sekcji dowiesz się, jak używać reguł zabezpieczeń Firebase do ochrony danych.
Najpierw przyjrzyjmy się bliżej regułom bezpieczeństwa, które napisaliśmy na początku tego laboratorium. Otwórz konsolę Firebase i kliknij Baza danych > Reguły na karcie 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;
}
}
}
Zmienna request
w regułach jest zmienną globalną dostępną we wszystkich regułach, a dodany przez nas warunek zapewnia, że żądanie jest uwierzytelniane, zanim użytkownicy będą mogli cokolwiek zrobić. Uniemożliwia to nieuwierzytelnionym użytkownikom wprowadzanie nieautoryzowanych zmian w danych za pomocą interfejsu Firestore API. To dobry początek, ale reguły Firestore pozwalają na znacznie więcej.
Chcemy ograniczyć możliwość pisania opinii, tak aby identyfikator użytkownika opinii musiał być zgodny z identyfikatorem uwierzytelnionego użytkownika. Dzięki temu użytkownicy nie mogą podszywać się pod inne osoby i dodawać fałszywych opinii.
Pierwsza instrukcja dopasowania pasuje do kolekcji podrzędnej o nazwie ratings
w dowolnym dokumencie należącym do kolekcji restaurants
. Warunek allow write
uniemożliwia przesłanie opinii, jeśli identyfikator użytkownika w opinii nie pasuje do identyfikatora użytkownika. Druga instrukcja dopasowania zezwala każdemu uwierzytelnionemu użytkownikowi na odczytywanie i zapisywanie informacji o restauracjach w bazie danych.
W przypadku naszych opinii to rozwiązanie sprawdza się bardzo dobrze, ponieważ za pomocą reguł zabezpieczeń wyraźnie określamy domyślną gwarancję, którą wcześniej wpisaliśmy w aplikacji – użytkownicy mogą pisać tylko własne opinie. Jeśli dodamy funkcję edytowania lub usuwania opinii, ten sam zestaw reguł uniemożliwi użytkownikom modyfikowanie lub usuwanie opinii innych użytkowników. Reguły Firestore można jednak stosować w bardziej szczegółowy sposób, aby ograniczać zapisywanie poszczególnych pól w dokumentach, a nie całych dokumentów. Możemy użyć tej funkcji, aby umożliwić użytkownikom aktualizowanie tylko ocen, średniej oceny i liczby ocen restauracji, co uniemożliwi złośliwym użytkownikom zmianę nazwy lub lokalizacji restauracji.
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;
}
}
}
Podzieliliśmy uprawnienie do zapisu na uprawnienia do tworzenia i aktualizowania, aby 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 utworzyliśmy na początku tego samouczka. Po zapisaniu restauracji nie można jednak zmienić jej nazwy, lokalizacji, ceny ani kategorii. Ostatnia reguła wymaga, aby każda operacja aktualizacji restauracji zachowywała tę samą nazwę, miasto, cenę i kategorię, które są już w bazie danych.
Więcej informacji o tym, co możesz zrobić z regułami zabezpieczeń, znajdziesz w dokumentacji.
9. Podsumowanie
W tym laboratorium dowiedziałeś się, jak wykonywać podstawowe i zaawansowane odczyty i zapisy w Firestore, a także jak zabezpieczyć dostęp do danych za pomocą reguł zabezpieczeń. Pełne rozwiązanie znajdziesz w codelab-complete
gałęzi.
Więcej informacji o Firestore znajdziesz w tych materiałach: