Cloud Firestore iOS Codelab'i

1. Genel Bakış

Gol sayısı

Bu codelab'de Swift ile iOS'te Firestore destekli bir restoran öneri uygulaması geliştireceksiniz. Bu derslere katılarak:

  1. iOS uygulamasından Firestore veri okuma ve yazma
  2. Firestore verilerindeki değişiklikleri gerçek zamanlı olarak dinleyin
  3. Firestore verilerinin güvenliğini sağlamak için Firebase Authentication ve güvenlik kurallarını kullanın
  4. Karmaşık Firestore sorgularını yazma

Ön koşullar

Bu codelab'i başlatmadan önce şunları yüklediğinizden emin olun:

  • Xcode sürüm 14.0 (veya üzeri)
  • CocoaPods 1.12.0 (veya üzeri)

2. Firebase konsol projesi oluşturun

Firebase'i projeye ekleyin

  1. Firebase konsoluna gidin.
  2. Yeni Proje Oluştur'u seçin ve projenize "Firestore iOS Codelab" adını verin.

3. Örnek Projeyi Alma

Kodu İndirin

Örnek projeyi klonlayıp proje dizininde pod update komutunu çalıştırarak başlayın:

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

FriendlyEats.xcworkspace komutunu Xcode'da açıp çalıştırın (Cmd+R). Uygulama doğru bir şekilde derlenmeli ve GoogleService-Info.plist dosyası eksik olduğundan başlatıldığında hemen kilitlenmelidir. Bu sorunu bir sonraki adımda düzelteceğiz.

Firebase'i ayarlayın

Yeni bir Firestore projesi oluşturmak için belgeleri uygulayın. Projenizi aldıktan sonra, Firebase konsolundan projenizin GoogleService-Info.plist dosyasını indirin ve Xcode projesinin köküne sürükleyin. Uygulamanın doğru şekilde yapılandırıldığından ve başlatma sırasında artık kilitlenmediğinden emin olmak için projeyi tekrar çalıştırın. Giriş yaptıktan sonra aşağıdaki örnekte gösterildiği gibi boş bir ekran göreceksiniz. Giriş yapamıyorsanız Firebase konsolunda Kimlik Doğrulama altında E-posta/Şifre oturum açma yöntemini etkinleştirdiğinizden emin olun.

d5225270159c040b.png

4. Verileri Firestore'a Yazma

Bu bölümde, uygulamanın kullanıcı arayüzünü doldurabilmek için bazı verileri Firestore'a yazacağız. Bu işlem Firebase konsolu üzerinden manuel olarak yapılabilir. Ancak temel bir Firestore yazma işlemini göstermek için bu işlemi uygulamanın içinde gerçekleştireceğiz.

Uygulamamızdaki ana model nesnesi bir restoran. Firestore verileri belgelere, koleksiyonlara ve alt koleksiyonlara ayrılır. Her bir restoranı, restaurants adlı üst düzey bir koleksiyonda belge olarak saklayacağız. Firestore veri modeli hakkında daha fazla bilgi edinmek için belgelerdeki belge ve koleksiyonlar hakkındaki bilgileri okuyun.

Firestore'a veri ekleyebilmek için önce restoran koleksiyonuyla ilgili bir referans almamız gerekir. Aşağıdakini, RestaurantsTableViewController.didTapPopulateButton(_:) yöntemindeki "for" döngüsüne ekleyin.

let collection = Firestore.firestore().collection("restaurants")

Artık bir koleksiyon referansımız olduğuna göre bazı verileri yazabiliriz. Aşağıdakini, eklediğimiz son kod satırının hemen arkasına 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. Belge verileri, Restaurant struct'tan aldığımız bir sözlükten geliyor.

Bitmek üzereyiz. Firestore'a doküman yazabilmemiz için önce Firestore'un güvenlik kurallarını açmamız ve veritabanımızın hangi bölümlerine hangi kullanıcıların yazabileceğ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 vereceğiz. Bu, üretim uygulamaları için biraz fazla ihtiyatlı bir uygulamadır ancak uygulama geliştirme sürecinde, deneme yaparken sürekli 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 sağlamlaştıracağınızı ve istenmeyen okuma ve yazma işlemlerini nasıl sınırlayacağınızı ele alacağız.

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

Güvenlik kurallarını daha sonra ayrıntılı olarak ele alacağız. Ancak aceleniz varsa güvenlik kuralları belgelerine göz atabilirsiniz.

Uygulamayı çalıştırın ve oturum açın. Ardından "Doldur"a dokunun. düğmesini tıklayabilirsiniz. Bu düğme bir grup restoran dokümanı oluşturur. Ancak bu işlemi henüz uygulamada görmezsiniz.

Ardından, Firebase konsolunda Firestore verileri sekmesine gidin. Artık restoranlar koleksiyonunda yeni girişleri göreceksiniz:

Screen Shot 2017-07-06 at 12.45.38 PM.png

Tebrikler, bir iOS uygulamasından Firestore'a veri yazdınız. Bir sonraki bölümde Firestore'dan veri almayı ve uygulamada görüntülemeyi öğreneceksiniz.

5. Firestore'dan alınan verileri göster

Bu bölümde, Firestore'dan veri almayı ve uygulamada görüntülemeyi öğreneceksiniz. İki temel adım, sorgu oluşturma ve anlık görüntü işleyici eklemedir. Bu dinleyici, sorguyla eşleşen mevcut tüm veriler hakkında bilgilendirilir ve güncellemeleri gerçek zamanlı olarak alır.

Öncelikle varsayılan, filtrelenmemiş restoran listesini sunacak sorguyu oluşturalım. RestaurantsTableViewController.baseQuery() uygulamasına göz atın:

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

Bu sorgu, "restoranlar" adlı üst düzey koleksiyondan en fazla 50 restoranı getirir. Artık bir sorgu aldığımıza göre, Firestore'dan uygulamamıza veri yüklemek için anlık görüntü dinleyici eklememiz gerekiyor. stopObserving() çağrısından hemen sonra RestaurantsTableViewController.observeQuery() yöntemine 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ü dinleyici ekler. Bu anlık görüntü işleyici, sunucuda 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 aktarmamız gerekmez. Bu anlık görüntü dinleyicisinin, sunucu tarafındaki bir değişiklik sonucu olarak herhangi bir zamanda başlatılabileceğini unutmayın. Bu nedenle, uygulamamızın değişiklikleri işleyebilmesi önemlidir.

Sözlüklerimizi struct ile eşledikten sonra (bkz. Restaurant.swift) verilerin görüntülenmesi yalnızca birkaç görünüm özelliği atamaktan ibarettir. Aşağıdaki satırları, RestaurantsTableViewController.swift bölgesindeki RestaurantTableViewCell.populate(restaurant:) içine 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ürlerinin toplanmasını 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ündüğünü doğrulayın. Bu bölümü başarıyla tamamladıysanız uygulamanız artık Cloud Firestore ile veri okuyup yazıyor demektir.

391c0259bf05ac25.png

6. Verileri Sıralama ve Filtreleme

Şu anda uygulamamızda bir restoran listesi görüntülenmektedir, ancak kullanıcının ihtiyaçlarına göre filtreleme yapması mümkün değildir. Bu bölümde, filtrelemeyi etkinleştirmek için Firestore'un gelişmiş sorgulama özelliğini kullanacaksınız.

Tüm Dim Sum restoranlarını getiren basit bir sorgu örneğini burada bulabilirsiniz:

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ıtlamalara uyan koleksiyon üyelerini indirmesini sağlar. Bu durumda yalnızca category öğesinin "Dim Sum" olduğu restoranlar indirilir.

Bu uygulamada kullanıcı, "İstanbul'da pizza" gibi belirli sorgular oluşturmak için birden çok filtre zinciri oluşturabilir. veya "Los Angeles'taki popülerlik derecesine göre sıralanmış deniz ürünleri".

RestaurantsTableViewController.swift dosyasını açın ve şu kod bloğunu query(withCategory:city:price:sortBy:) kodunun 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 göre 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 gereksinimlerini karşılayan restoranları döndürecektir.

Projenizi çalıştırın ve fiyata, şehre ve kategoriye göre filtre uygulayabildiğ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 dizinleri gerektirmesidir. Sorgularda dizin gerekliliği, Firestore'un geniş ölçekte hızlı olmasını sağlar. Hata mesajındaki bağlantıyı 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. Bir işlemde veri yazma

Bu bölümde, kullanıcıların restoranlara yorum göndermelerine olanak tanıyacağız. Bugüne kadar, tüm yazılarımız atomik ve nispeten daha basitti. Bu adımlardan herhangi biri hatalı olursa büyük olasılıkla kullanıcıdan işlemi yeniden denemesini veya otomatik olarak yeniden denemesini isteriz.

Bir restorana derecelendirme eklemek için birden fazla okuma ve yazma işlemini koordine etmemiz gerekir. İlk olarak yorumun gönderilmesi ve ardından restoranın puan sayısı ile ortalama puanının güncellenmesi gerekiyor. Bunlardan biri başarısız olursa diğeri başarısız olursa, veritabanımızın bir bölümündeki verilerin diğerindeki 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ıyarak verilerimizin tutarlı kalmasını sağlayan işlem işlevi sunar.

RestaurantDetailViewController.reviewController(_:didSubmitFormWithReview:) içindeki tüm allow 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 atom güncellemesi olarak ele alınacaktır. Güncelleme sunucuda başarısız olursa Firestore otomatik olarak birkaç kez yeniden dener. Bu, hata koşulumuzun muhtemelen tekrar tekrar ortaya çıkan tek bir hata olduğu anlamına gelir. Örneğin, cihaz tamamen çevrimdışıysa veya kullanıcının yazmaya çalıştığı yola yazma yetkisi yoksa.

8. Güvenlik kuralları

Uygulamamızın kullanıcıları, veritabanımızdaki her veri parçasını okuyup yazamamalıdır. Örneğin, bir restoranın puanlarını herkes görebilmelidir, ancak yalnızca kimliği doğrulanmış bir kullanıcının puan yayınlamasına izin verilmelidir. İstemcide iyi bir 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 verilerimizi korumak için Firebase güvenlik kurallarının nasıl kullanılacağını öğreneceksiniz.

Öncelikle, codelab'in başında yazdığımız güvenlik kurallarına daha ayrıntılı bir şekilde bakalım. Firebase konsolunu açıp Veritabanı > Firestore sekmesindeki kurallar.

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

Yukarıdaki kurallarda yer alan request değişkeni, tüm kurallarda bulunan bir genel değişkendir ve eklediğimiz koşul, 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 verilerinizde yetkisiz değişiklik yapmak için Firestore API'yi kullanmasını engeller. Bu iyi bir başlangıç, ancak çok daha güçlü şeyler yapmak için Firestore kurallarını kullanabiliriz.

Yorum yazma işlemlerini, yorumun kullanıcı kimliğinin kimliği doğrulanmış kullanıcının kimliğiyle eşleşmesi için kısıtlayalım. Bu, kullanıcıların birbirlerinin kimliğine bürünmemesini ve sahte yorumlar bırakmasını önler. Güvenlik kurallarınızı aşağıdakilerle değiştirin:

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

İlk eşleşme ifadesi, restaurants koleksiyonuna ait olan herhangi bir dokümanın ratings adlı alt koleksiyonla eşleşir. Ardından, allow write koşulu, yorumun kullanıcı kimliği kullanıcının kimliğiyle eşleşmiyorsa incelemenin gönderilmesini engeller. İkinci eşleşme ifadesi, kimliği doğrulanmış tüm kullanıcıların restoranları okumasına ve veritabanına yazmasına olanak tanır.

Uygulamamıza daha önce yazdığımız, kullanıcıların yalnızca kendi incelemelerini yazabileceğine dair örtülü garantiyi açıkça belirtmek için güvenlik kurallarından yararlandığımızdan, bu uygulama incelemelerimizde çok işe yarıyor. Yorumlar için bir düzenleme veya silme işlevi ekleyecek olsaydık, aynı kural kümesi de kullanıcıların diğer kullanıcıların hesaplarını değiştirmesini veya silmesini engelleyecekti. dikkate alın. Ancak Firestore kuralları, tüm dokümanların kendisi yerine dokümanların içindeki bağımsız alanlarda yazma işlemlerini sınırlandırmak için daha ayrıntılı bir şekilde de kullanılabilir. Kötü amaçlı bir kullanıcının bir restoranın adını veya konumunu değiştirmesi ihtimalini ortadan kaldırarak kullanıcıların bir restoranla ilgili yalnızca puan, ortalama puan ve puan sayısını güncellemelerine olanak tanımak için bunu kullanabiliriz.

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

Hangi işlemlere izin verilmesi gerektiği konusunda daha ayrıntılı bilgi verebilmek için burada yazma iznimizi oluşturma ve güncelleme işlemlerine ayırdık. Herhangi bir kullanıcı, codelab'in başında oluşturduğumuz Doldur düğmesinin işlevini koruyarak veritabanına restoran yazabilir ancak bir restoranın adı, konumu, fiyatı ve kategorisi değiştirilemez. Daha açık belirtmek gerekirse son kural, veritabanında mevcut olan alanlarla aynı ad, şehir, fiyat ve kategoriyi korumak için herhangi bir restoran güncelleme işleminin yapılması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 ileri düzey okuma ve yazma işlemlerinin yanı sıra güvenlik kurallarıyla veri erişiminin nasıl güvence altına alınacağını öğ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: