1. Genel Bakış
Gol sayısı
Bu codelab'de, Swift ile iOS'te Firestore destekli bir restoran önerisi uygulaması oluşturacaksınız. Bu eğitimde şunları öğreneceksiniz:
- iOS uygulamasından Firestore'a veri okuma ve yazma
- Firestore verilerindeki değişiklikleri anlık olarak dinleme
- Firestore verilerinin güvenliğini sağlamak için Firebase Authentication ve güvenlik kurallarını kullanma
- Karmaşık Firestore sorguları yazma
Ön koşullar
Bu codelab'e başlamadan önce aşağıdakileri yüklediğinizden emin olun:
- Xcode 14.0 veya sonraki bir sürüm
- CocoaPods 1.12.0 veya sonraki sürümler
2. Örnek Projeyi Alma
Kodu indirme
İlk olarak örnek projeyi klonlayın ve proje dizininde pod update
komutunu çalıştırın:
git clone https://github.com/firebase/friendlyeats-ios cd friendlyeats-ios pod update
Xcode'da FriendlyEats.xcworkspace
dosyasını açıp çalıştırın (Cmd+R). Uygulama, GoogleService-Info.plist
dosyası eksik olduğundan doğru şekilde derlenmeli ve başlatıldığında hemen kilitlenmelidir. Bu durumu sonraki adımda düzelteceğiz.
3. Firebase'i ayarlama
Firebase projesi oluşturma
- Google Hesabınızı kullanarak Firebase konsolunda oturum açın.
- Yeni bir proje oluşturmak için düğmeyi tıklayın ve ardından bir proje adı girin (örneğin,
FriendlyEats
).
- Devam'ı tıklayın.
- İstenirse Firebase şartlarını inceleyip kabul edin ve Devam'ı tıklayın.
- (İsteğe bağlı) Firebase konsolunda yapay zeka yardımını etkinleştirin ("Firebase'de Gemini" olarak adlandırılır).
- Bu codelab için Google Analytics'e ihtiyacınız yoktur. Bu nedenle, Google Analytics seçeneğini devre dışı bırakın.
- Proje oluştur'u tıklayın, projenizin hazırlanmasını bekleyin ve ardından Devam'ı tıklayın.
Uygulamanızı Firebase'e bağlama
Yeni Firebase projenizde bir iOS uygulaması oluşturun.
Projenizin GoogleService-Info.plist
dosyasını Firebase konsolundan indirip Xcode projesinin kök dizinine sürükleyin. Uygulamanın doğru şekilde yapılandırıldığından ve başlatılırken artık kilitlenmediğinden emin olmak için projeyi tekrar çalıştırın. Oturum açtıktan sonra, aşağıdaki örnekte olduğu gibi boş bir ekran görmeniz gerekir. Giriş yapamıyorsanız Firebase konsolunda Kimlik Doğrulama bölümünde E-posta/Şifre ile giriş yöntemini etkinleştirdiğinizden emin olun.
4. Firestore'a veri yazma
Bu bölümde, uygulama kullanıcı arayüzünü doldurabilmek için Firestore'a bazı veriler yazacağız. Bu işlem Firebase konsolu üzerinden manuel olarak yapılabilir ancak temel bir Firestore yazma işlemini göstermek için bunu uygulamanın kendisinde yapacağız.
Uygulamamızdaki ana model nesnesi restorandır. Firestore verileri belgelere, koleksiyonlara ve alt koleksiyonlara ayrılır. Her restoranı restaurants
adlı üst düzey bir koleksiyonda belge olarak saklayacağız. Firestore veri modeli hakkında daha fazla bilgi edinmek istiyorsanız dokümanlarda belgeler ve koleksiyonlar hakkında bilgi edinin.
Firestore'a veri ekleyebilmemiz için restoran koleksiyonuna referans almamız gerekir. RestaurantsTableViewController.didTapPopulateButton(_:)
yöntemindeki iç for döngüsüne aşağıdakileri ekleyin.
let collection = Firestore.firestore().collection("restaurants")
Artık bir koleksiyon referansımız olduğuna göre bazı veriler yazabiliriz. Eklediğimiz son kod satırının hemen ardından aşağıdakileri ekleyin:
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)
Yukarıdaki kod, restoranlar koleksiyonuna yeni bir doküman ekler. Doküman verileri, bir sözlükten gelir. Bu sözlüğü, bir Restaurant yapısından alırız.
Neredeyse tamamladık. Firestore'a doküman yazabilmek için Firestore'un güvenlik kurallarını açmamız ve veritabanımızın hangi bölümlerinin hangi kullanıcılar tarafından yazılabilir olması gerektiğini açıklamamız gerekiyor. Şu an için yalnızca kimliği doğrulanmış kullanıcıların veritabanının tamamını okumasına ve yazmasına izin veriyoruz. Bu, üretim uygulaması için biraz fazla izin verici olsa da uygulama oluşturma sürecinde, deneme yaparken sürekli olarak kimlik doğrulama sorunlarıyla karşılaşmamak için yeterince rahat bir şey istiyoruz. Bu codelab'in sonunda, güvenlik kurallarınızı nasıl güçlendireceğiniz ve istenmeyen okuma ve yazma işlemlerinin olasılığını nasıl sınırlayacağınız hakkında konuşacağız.
Firebase konsolunun Kurallar sekmesinde aşağıdaki kuralları ekleyin ve Yayınla'yı tıklayın.
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; } } }
Güvenlik kurallarını daha sonra ayrıntılı olarak ele alacağız. Ancak aceleniz varsa güvenlik kuralları dokümanlarına göz atabilirsiniz.
Uygulamayı çalıştırın ve oturum açın. Ardından sol üstteki "Doldur" düğmesine dokunun. Bu işlem, uygulamada henüz görmeyeceğiniz bir grup restoran dokümanı oluşturur.
Ardından Firebase konsolunda Firestore verileri sekmesine gidin. Artık restoran koleksiyonunda yeni girişler görmeniz gerekir:
Tebrikler, iOS uygulamasından Firestore'a veri yazdınız. Sonraki bölümde, Firestore'dan verileri nasıl alacağınızı ve uygulamada nasıl görüntüleyeceğinizi öğreneceksiniz.
5. Firestore'daki Verileri Görüntüleme
Bu bölümde, Firestore'dan nasıl veri alacağınızı ve bu verileri uygulamada nasıl göstereceğinizi öğreneceksiniz. Bu işlemde iki temel adım vardır: sorgu oluşturma ve anlık görüntü dinleyici ekleme. Bu dinleyici, sorguyla eşleşen tüm mevcut veriler hakkında bilgilendirilir ve güncellemeleri anlık olarak alır.
Öncelikle, varsayılan ve filtrelenmemiş restoran listesini sunacak sorguyu oluşturalım. RestaurantsTableViewController.baseQuery()
uygulamasının nasıl yapıldığına göz atın:
return Firestore.firestore().collection("restaurants").limit(to: 50)
Bu sorgu, "restoranlar" adlı üst düzey koleksiyonda bulunan en fazla 50 restoranı alır. Artık bir sorgumuz olduğuna göre, Firestore'dan veri yüklemek için uygulamamıza bir anlık görüntü dinleyicisi eklememiz gerekiyor. RestaurantsTableViewController.observeQuery()
yöntemine, stopObserving()
çağrısından hemen sonra aşağıdaki kodu ekleyin.
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()
}
Yukarıdaki kod, Firestore'dan koleksiyonu indirir ve yerel olarak bir dizide depolar. addSnapshotListener(_:)
çağrısı, sorguya bir anlık görüntü dinleyicisi ekler. Bu dinleyici, sunucudaki veriler her değiştiğinde görünüm denetleyicisini günceller. Güncellemeleri otomatik olarak alırız ve değişiklikleri manuel olarak göndermemiz gerekmez. Bu anlık görüntü dinleyicinin, sunucu tarafında yapılan bir değişiklik sonucunda herhangi bir zamanda çağrılabileceğini unutmayın. Bu nedenle, uygulamamızın değişiklikleri işleyebilmesi önemlidir.
Sözlüklerimizi yapılandırılmış verilere eşledikten sonra (Restaurant.swift
bölümüne bakın) verileri görüntülemek için birkaç görünüm özelliği atamanız yeterlidir. RestaurantsTableViewController.swift
içindeki RestaurantTableViewCell.populate(restaurant:)
dosyasına aşağıdaki satırları ekleyin.
nameLabel.text = restaurant.name
cityLabel.text = restaurant.city
categoryLabel.text = restaurant.category
starsView.rating = Int(restaurant.averageRating.rounded())
priceLabel.text = priceString(from: restaurant.price)
Bu doldurma yöntemi, tablo görünümü veri kaynağının tableView(_:cellForRowAtIndexPath:)
yönteminden çağrılır. Bu yöntem, önceki değer türleri koleksiyonunun tek tek tablo görünümü hücreleriyle eşlenmesini sağlar.
Uygulamayı tekrar çalıştırın ve konsolda daha önce gördüğümüz restoranların artık simülatörde veya cihazda görünür olduğunu doğrulayın. Bu bölümü başarıyla tamamladıysanız uygulamanız artık Cloud Firestore ile veri okuyup yazıyor.
6. Verileri Sıralama ve Filtreleme
Uygulamamız şu anda restoranların listesini gösteriyor ancak kullanıcının ihtiyaçlarına göre filtreleme yapabileceği bir yol yok. Bu bölümde, filtrelemeyi etkinleştirmek için Firestore'un gelişmiş sorgu özelliğini kullanacaksınız.
Tüm Dim Sum restoranlarını getirmek için basit bir sorgu örneği:
let filteredQuery = query.whereField("category", isEqualTo: "Dim Sum")
Adından da anlaşılacağı gibi, whereField(_:isEqualTo:)
yöntemi, sorgumuzun yalnızca alanları belirlediğimiz kısıtlamaları karşılayan koleksiyon üyelerini indirmesini sağlar. Bu durumda, yalnızca category
değerinin "Dim Sum"
olduğu restoranlar indirilir.
Bu uygulamada kullanıcı, "San Francisco'da pizza" veya "Los Angeles'ta popülerliğe göre sıralanmış deniz ürünleri" gibi belirli sorgular oluşturmak için birden fazla filtreyi birleştirebilir.
RestaurantsTableViewController.swift
bağlantısını açın ve aşağıdaki kod bloğunu query(withCategory:city:price:sortBy:)
öğesinin ortasına ekleyin:
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)
}
Yukarıdaki snippet, kullanıcı girişine dayalı tek bir bileşik sorgu oluşturmak için birden fazla whereField
ve order
ifadesi ekler. Artık sorgumuz yalnızca kullanıcının şartlarını karşılayan restoranları döndürecek.
Projenizi çalıştırın ve fiyata, şehre ve kategoriye göre filtreleme yapabildiğinizi doğrulayın (kategori ve şehir adlarını tam olarak yazdığınızdan emin olun). Test sırasında günlüklerinizde aşağıdaki gibi hatalar görebilirsiniz:
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=...}
Bunun nedeni, Firestore'un çoğu bileşik sorgu için dizin gerektirmesidir. Sorgularda dizinlerin zorunlu tutulması, Firestore'un ölçeklendirme sırasında hızlı kalmasını sağlar. Bağlantıyı hata mesajından açtığınızda, Firebase konsolunda dizin oluşturma kullanıcı arayüzü doğru parametreler doldurulmuş şekilde otomatik olarak açılır. Firestore'daki dizinler hakkında daha fazla bilgi edinmek için belgeleri inceleyin.
7. İşlemde veri yazma
Bu bölüme, kullanıcıların restoranlara yorum gönderebilme özelliğini ekleyeceğiz. Şimdiye kadar tüm yazma işlemlerimiz atomik ve nispeten basitti. Bunlardan herhangi biri hata verirse kullanıcıdan yeniden denemesini isteriz veya otomatik olarak yeniden deneriz.
Bir restorana puan eklemek için birden fazla okuma ve yazma işlemini koordine etmemiz gerekir. Öncelikle değerlendirmenin kendisi gönderilmeli, ardından restoranın değerlendirme sayısı ve ortalama puanı güncellenmelidir. Bunlardan biri başarısız olursa ancak diğeri başarısız olmazsa veritabanımızın bir bölümündeki verilerin başka bir bölümündeki verilerle eşleşmediği tutarsız bir durumda kalırız.
Neyse ki Firestore, tek bir atomik işlemde birden fazla okuma ve yazma işlemi yapmamıza olanak tanıyan işlem işlevi sunarak verilerimizin tutarlı kalmasını sağlar.
RestaurantDetailViewController.reviewController(_:didSubmitFormWithReview:)
dosyasındaki tüm let bildirimlerinin altına aşağıdaki kodu ekleyin.
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)
}
}
}
Güncelleme bloğunun içinde, işlem nesnesi kullanılarak yapılan tüm işlemler Firestore tarafından tek bir atomik güncelleme olarak değerlendirilir. Güncelleme sunucuda başarısız olursa Firestore otomatik olarak birkaç kez yeniden dener. Bu, hata koşulumuzun büyük olasılıkla tekrar tekrar oluşan tek bir hata olduğu anlamına gelir. Örneğin, cihaz tamamen çevrimdışıysa veya kullanıcı, yazmaya çalıştığı yola yazma yetkisine sahip değilse.
8. Güvenlik kuralları
Uygulamamızın kullanıcıları, veritabanımızdaki her bir veriyi okuyup yazamamalıdır. Örneğin, herkes bir restoranın puanlarını görebilmelidir ancak yalnızca kimliği doğrulanmış bir kullanıcının puan yayınlamasına izin verilmelidir. İstemcide iyi kod yazmak yeterli değildir. Tamamen güvenli olmak için veri güvenliği modelimizi arka uçta belirtmemiz gerekir. Bu bölümde, verilerimizi korumak için Firebase güvenlik kurallarını nasıl kullanacağımızı öğreneceğiz.
Öncelikle, codelab'in başında yazdığımız güvenlik kurallarını daha ayrıntılı bir şekilde inceleyelim. Firebase konsolunu açın ve Firestore sekmesinde Database > Rules'a (Veritabanı > Kurallar) gidin.
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;
}
}
}
Kurallardaki request
değişkeni, tüm kurallarda kullanılabilen genel bir değişkendir. Eklediğimiz koşullu ifade, kullanıcıların herhangi bir işlem yapmasına izin verilmeden önce isteğin kimliğinin doğrulanmasını sağlar. Bu, kimliği doğrulanmamış kullanıcıların Firestore API'yi kullanarak verilerinizde yetkisiz değişiklikler yapmasını engeller. Bu iyi bir başlangıç olsa da çok daha güçlü işlemler yapmak için Firestore kurallarını kullanabiliriz.
Yorum yazma işlemini kısıtlamak istiyoruz. Bu nedenle, yorumun kullanıcı kimliği, kimliği doğrulanmış kullanıcının kimliğiyle eşleşmelidir. Bu sayede kullanıcılar birbirinin kimliğine bürünemez ve sahte yorum bırakamaz.
İlk eşleşme ifadesi, restaurants
koleksiyonuna ait herhangi bir belgenin ratings
adlı alt koleksiyonuyla eşleşir. allow write
koşulu, yorumun kullanıcı kimliği kullanıcının kimliğiyle eşleşmediği takdirde yorum gönderilmesini engeller. İkinci eşleşme ifadesi, kimliği doğrulanmış tüm kullanıcıların veritabanına restoran okumasına ve yazmasına olanak tanır.
Bu, incelemelerimiz için gerçekten iyi bir çözüm oldu. Çünkü daha önce uygulamamıza yazdığımız, kullanıcıların yalnızca kendi incelemelerini yazabileceği şeklindeki örtülü garantiyi açıkça belirtmek için güvenlik kurallarını kullandık. Yorumlar için düzenleme veya silme işlevi eklersek aynı kurallar, kullanıcıların diğer kullanıcıların yorumlarını değiştirmesini veya silmesini de engeller. Ancak Firestore kuralları, dokümanların tamamı yerine dokümanlardaki tek tek alanlara yazma işlemlerini sınırlamak için daha ayrıntılı bir şekilde de kullanılabilir. Bunu, kullanıcıların yalnızca bir restoranın puanlarını, ortalama puanını ve puan sayısını güncellemesine izin vermek için kullanabiliriz. Böylece kötü niyetli kullanıcıların restoran adını veya konumunu değiştirmesi önlenir.
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;
}
}
}
Burada, hangi işlemlerin izin verilmesi gerektiği konusunda daha net olabilmek için yazma iznimizi oluşturma ve güncelleme olarak ayırdık. Herhangi bir kullanıcı, restoranları veritabanına yazabilir. Bu işlem, codelab'in başında oluşturduğumuz Populate (Doldur) düğmesinin işlevini korur. Ancak bir restoran yazıldıktan sonra adı, konumu, fiyatı ve kategorisi değiştirilemez. Daha spesifik olarak, son kural, herhangi bir restoran güncelleme işleminin veritabanındaki mevcut alanların adını, şehrini, fiyatını ve kategorisini korumasını gerektirir.
Güvenlik kurallarıyla neler yapabileceğiniz hakkında daha fazla bilgi edinmek için belgelere göz atın.
9. Sonuç
Bu codelab'de, Firestore ile temel ve gelişmiş okuma ve yazma işlemlerinin yanı sıra güvenlik kurallarıyla veri erişimini güvenli hale getirme hakkında bilgi edindiniz. Çözümün tamamını codelab-complete
dalında bulabilirsiniz.
Firestore hakkında daha fazla bilgi edinmek için aşağıdaki kaynakları ziyaret edin: