1. Genel Bakış
Gol sayısı
Bu codelab'de, iOS'te Swift ile Firestore destekli bir restoran öneri uygulaması oluşturacaksınız. Aşağıdakileri nasıl yapacağınızı öğ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 kod laboratuvarını başlatmadan önce şunları yüklediğinizden emin olun:
- Xcode 14.0 (veya sonraki sürümler)
- CocoaPods 1.12.0 (veya sonraki sürümler)
2. Firebase Konsolu projesi oluşturma
Firebase'i projeye ekleme
- Firebase konsoluna gidin.
- Yeni Proje Oluştur'u seçin ve projenizi "Firestore iOS Codelab" olarak adlandırın.
3. Örnek Projeyi İndirme
Kodu indirme
Örnek projeyi klonlayarak ve proje dizininde pod update
çalıştırarak başlayı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ğu için doğru şekilde derlenir ve başlatılır başlamaz kilitlenir. Bu sorunu bir sonraki adımda düzelteceğiz.
Firebase'i ayarlama
Yeni bir Firestore projesi oluşturmak için dokümanları inceleyin. Projenizi oluşturduktan sonra Firebase konsolundan projenizin GoogleService-Info.plist
dosyasını indirip Xcode projesinin köküne sürükleyin. Uygulamanın doğru şekilde yapılandırıldığından ve artık başlatılırken kilitlenmediğinden emin olmak için projeyi tekrar çalıştırın. Oturum açtıktan sonra aşağıdaki örnekte gösterilen gibi boş bir ekran görürsünüz. Giriş yapamıyorsanız Firebase konsolunda Kimlik Doğrulama bölümünde E-posta/Şifre oturum açma 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 içinde yapacağız.
Uygulamamızdaki ana model nesnesi bir restorandır. Firestore verileri dokümanlara, koleksiyonlara ve alt koleksiyonlara bölünür. Her restoranı restaurants
adlı üst düzey bir koleksiyonda doküman olarak depolarız. Firestore veri modeli hakkında daha fazla bilgi edinmek istiyorsanız dokümanlar bölümündeki belgeler ve koleksiyonlar hakkındaki bilgileri okuyun.
Firestore'a veri ekleyebilmek için restaurants koleksiyonunun referansını almamız gerekir. RestaurantsTableViewController.didTapPopulateButton(_:)
yöntemindeki iç for döngüsüne aşağıdakileri ekleyin.
let collection = Firestore.firestore().collection("restaurants")
Bir koleksiyon referansı oluşturduğumuza göre bazı veriler yazabiliriz. Eklediğimiz son kod satırının hemen sonrasına 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, restaurants koleksiyonuna yeni bir doküman ekler. Doküman verileri, bir restoran yapısından aldığımız bir sözlükten gelir.
Hemen hemen hazırız. Firestore'a belge yazabilmemiz 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 tanımlamamız gerekiyor. Şu anda yalnızca kimliği doğrulanmış kullanıcıların veritabanının tamamında okuma ve yazma yapmasına izin vereceğiz. Bu, üretim uygulaması için biraz fazla izin vericidir ancak uygulama oluşturma sürecinde, deneme yaparken sürekli kimlik doğrulama sorunlarıyla karşılaşmamak için yeterince esnek bir yapı isteriz. Bu kod laboratuvarının sonunda, güvenlik kurallarınızı nasıl daha da güçlendireceğinizden ve istenmeyen okuma ve yazma olasılığını nasıl sınırlayacağınızdan bahsedeceğiz.
Firebase konsolunun Kurallar sekmesinde aşağıdaki kuralları ekleyin ve ardından 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 ayrıntılı olarak ele alacağız. Ancak aceleniz varsa güvenlik kuralları dokümanlarına göz atın.
Uygulamayı çalıştırın ve oturum açın. Ardından, sol üstteki "Doldur" düğmesine dokunun. Bu işlem, bir grup restoran belgesi oluşturur ancak bu belgeleri henüz uygulamada görmezsiniz.
Ardından Firebase konsolunda Firestore verileri sekmesine gidin. Artık restoran koleksiyonunda yeni girişler göreceksiniz:
Tebrikler, bir iOS uygulamasından Firestore'a veri yazdınız. Sonraki bölümde, Firestore'dan nasıl veri alacağınızı ve bunları 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 bunları uygulamada nasıl görüntüleyeceğinizi öğreneceksiniz. İki önemli adım, sorgu oluşturmak ve anlık görüntü dinleyici eklemektir. Bu dinleyici, sorguyla eşleşen mevcut tüm veriler hakkında bilgilendirilir ve gerçek zamanlı olarak güncelleme alır.
Öncelikle, filtrelenmemiş varsayılan restoran listesini sunacak sorguyu oluşturalım. RestaurantsTableViewController.baseQuery()
özelliğinin uygulanmasına göz atın:
return Firestore.firestore().collection("restaurants").limit(to: 50)
Bu sorgu, "restaurants" adlı üst düzey koleksiyondan en fazla 50 restoran alır. Sorgumuz hazır olduğuna göre, Firestore'daki verileri uygulamamıza yüklemek için bir anlık görüntü dinleyici eklememiz gerekiyor. Aşağıdaki kodu RestaurantsTableViewController.observeQuery()
yöntemine, stopObserving()
çağrısından hemen sonra 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, koleksiyonu Firestore'dan indirir ve yerel olarak bir dizgede depolar. addSnapshotListener(_:)
çağrısı, sunucuda veriler her değiştiğinde görüntü kontrol cihazını güncelleyecek bir anlık görüntü dinleyicisi sorguya ekler. Güncellemeleri otomatik olarak alırız ve değişiklikleri manuel olarak göndermemiz gerekmez. Bu anlık görüntü dinleyicisinin, 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ılarla eşledikten sonra (Restaurant.swift
bölümüne bakın), verileri görüntülemek için birkaç görüntüleme özelliği atamanız yeterlidir. RestaurantsTableViewController.swift
dosyasında RestaurantTableViewCell.populate(restaurant:)
bölümüne 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 koleksiyonunu tek tek tablo görünümü hücreleriyle eşler.
Uygulamayı tekrar çalıştırın ve daha önce konsolda gördüğümüz restoranların artık simülatörde veya cihazda görünür olup olmadığını doğrulayın. Bu bölümü başarıyla tamamladıysanız uygulamanız artık Cloud Firestore ile veri okuyup yazabiliyordur.
6. Verileri sıralama ve filtreleme
Şu anda uygulamamızda restoranların listesi gösteriliyor ancak kullanıcının ihtiyaçlarına göre filtreleme yapması mümkün değil. Bu bölümde, filtrelemeyi etkinleştirmek için Firestore'un gelişmiş sorgulamasını kullanacaksınız.
Tüm Dim Sum restoranlarını getirmek için basit bir sorgu örneğini aşağıda bulabilirsiniz:
let filteredQuery = query.whereField("category", isEqualTo: "Dim Sum")
Adından da anlaşılacağı gibi whereField(_:isEqualTo:)
yöntemi, sorgumuzun yalnızca alanların belirlediğimiz kısıtlamaları karşıladığı 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ülerlik sırasına göre deniz ürünleri" gibi belirli sorgular oluşturmak için birden fazla filtreyi birbirine bağlayabilir.
RestaurantsTableViewController.swift
dosyasını açın ve query(withCategory:city:price:sortBy:)
dosyasının ortasına aşağıdaki kod bloğunu 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 göre tek bir karma sorgu oluşturmak için birden fazla whereField
ve order
yan tümcesi ekler. Artık sorgumuz yalnızca kullanıcının şartlarına uyan restoranları döndürecek.
Projenizi çalıştırın ve fiyata, şehre ve kategoriye göre filtreleyebildiğ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. Sorgular için dizin zorunluluğu, Firestore'un büyük ölçekte hızlı kalmasını sağlar. Hata mesajındaki bağlantıyı açtığınızda Firebase konsolunda doğru parametrelerin doldurulduğu dizin oluşturma kullanıcı arayüzü otomatik olarak açılır. Firestore'daki dizinler hakkında daha fazla bilgi edinmek için dokümanları ziyaret edin.
7. İşlemde veri yazma
Bu bölümde, kullanıcıların restoranlara yorum gönderme özelliğini ekleyeceğiz. Bugüne kadar tüm yazma işlemlerimiz atomik ve nispeten basitti. Bunlardan herhangi birinde hata oluşursa kullanıcıdan bunları 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 yorumun gönderilmesi, ardından restoranın puan sayısının ve ortalama puanının güncellenmesi gerekir. Bunlardan biri başarısız olurken diğeri başarılı olursa veritabanımızın bir kısmındaki verilerin diğer kısmındaki 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 yapmamıza olanak tanıyan işlem işlevi sunarak verilerinizin tutarlı kalmasını sağlar.
RestaurantDetailViewController.reviewController(_:didSubmitFormWithReview:)
dosyasında, 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 nesnesini kullanarak yaptığımız tüm işlemler Firestore tarafından tek bir atomik güncelleme olarak değerlendirilir. Güncelleme sunucuda başarısız olursa Firestore, güncellemeyi otomatik olarak birkaç kez tekrar dener. Bu, hata durumunun büyük olasılıkla tekrar tekrar meydana gelen tek bir hata olduğu anlamına gelir. Örneğin, cihaz tamamen çevrimdışıysa veya kullanıcı yazmaya çalıştığı yola yazma yetkisi yoksa bu durum ortaya çıkar.
8. Güvenlik kuralları
Uygulamamızın kullanıcıları, veritabanımızdaki her veri parçasını okuyup yazamamalıdır. Örneğin, herkes bir restoranın puanlarını görebilir ancak yalnızca kimliği doğrulanmış bir kullanıcının puan yayınlamasına izin verilmelidir. Müşteride iyi kod yazmak yeterli değildir. Tamamen güvenli olması için arka uçta veri güvenliği modelimizi belirtmemiz gerekir. Bu bölümde, verilerinizi korumak için Firebase güvenlik kurallarını nasıl kullanacağınızı öğreneceksiniz.
Öncelikle, kod laboratuvarının başında yazdığımız güvenlik kurallarını daha ayrıntılı bir şekilde inceleyelim. Firebase konsolunu açıp Veritabanı > Firestore sekmesinde Kurallar'a 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 vermeden önce isteğin kimliğinin doğrulanmasını sağlar. Bu, kimliği doğrulanmamış kullanıcıların verileriniz üzerinde yetkisiz değişiklikler yapmak için Firestore API'yi kullanmasını engeller. Bu iyi bir başlangıç ancak çok daha güçlü işler yapmak için Firestore kurallarını kullanabiliriz.
Yorum yazma işlemini, yorumun kullanıcı kimliğinin kimliği doğrulanmış kullanıcının kimliğiyle eşleşmesi gerektiği şekilde kısıtlamak istiyoruz. Bu sayede kullanıcılar birbirlerinin kimliğine bürünerek sahte yorum bırakamaz.
İlk eşleşme ifadesi, restaurants
koleksiyonuna ait herhangi bir belgenin ratings
adlı alt koleksiyonuyla eşleşir. Ardından allow write
koşulu, yorumun kullanıcı kimliği kullanıcının kimliğiyle eşleşmezse yorumun gönderilmesini engeller. İkinci eşleşme ifadesi, kimliği doğrulanmış tüm kullanıcıların restoranları veritabanında okumasına ve yazmaya olanak tanır.
Uygulamamıza daha önce yazdığımız ve kullanıcıların yalnızca kendi yorumlarını yazabilecekleri şeklindeki zımni garantiyi açıkça belirtmek için güvenlik kurallarını kullandığımızdan, bu yöntem yorumlarımız için çok işe yaradı. 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ümanlar içindeki belirli alanlara yapılan yazma işlemlerini sınırlamak için daha ayrıntılı bir şekilde de kullanılabilir. Bu özelliği kullanarak kullanıcıların yalnızca bir restoranın puanlarını, ortalama puanını ve puan sayısını güncellemesine izin verebiliriz. Böylece, kötü amaçlı bir kullanıcının restoran adını veya konumunu değiştirmesi mümkün olmaz.
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şlemlere izin verilmesi gerektiği konusunda daha net olabilmemiz için yazma iznimizi oluşturma ve güncelleme olarak ikiye ayırdık. Her kullanıcı, kod alanının başında oluşturduğumuz Doldur düğmesinin işlevini koruyarak veritabanına restoran yazabilir. Ancak bir restoran yazıldıktan sonra adı, konumu, fiyatı ve kategorisi değiştirilemez. Daha açık belirtmek gerekirse son kural, restoran güncelleme işlemlerinin veritabanındaki mevcut alanlarla aynı adı, şehri, fiyatı ve kategoriyi korumasını gerektirir.
Güvenlik kurallarıyla neler yapabileceğiniz hakkında daha fazla bilgi edinmek için dokümanlara 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 nasıl güvence altına alacağınızı öğrendiniz. Çö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: