Cloud Firestore iOS Codelab

1. Genel Bakış

Hedefler

Bu codelab'de Swift'de iOS üzerinde Firestore destekli bir restoran öneri uygulaması oluşturacaksınız. Nasıl yapılacağını öğreneceksiniz:

  1. Bir iOS uygulamasından Firestore'a 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 Kimlik Doğrulamasını ve güvenlik kurallarını kullanın
  4. Karmaşık Firestore sorguları yazın

Önkoşullar

Bu codelab'e başlamadan önce aşağıdakileri yüklediğinizden emin olun:

  • Xcode sürüm 14.0 (veya üzeri)
  • CocoaPod'lar 1.12.0 (veya üstü)

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 Alın

Kodu İndir

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

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

FriendlyEats.xcworkspace Xcode'da açın ve çalıştırın (Cmd+R). GoogleService-Info.plist dosyası eksik olduğundan uygulama doğru şekilde derlenmeli ve başlatıldığında hemen çökmelidir. Bir sonraki adımda bunu düzelteceğiz.

Firebase'i kurun

Yeni bir Firestore projesi oluşturmak için belgeleri izleyin. Projenizi aldıktan sonra, projenizin GoogleService-Info.plist dosyasını Firebase konsolundan 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 yeniden çalıştırın. Giriş yaptıktan sonra aşağıdaki örnekteki gibi boş bir ekran görmelisiniz. Giriş yapamıyorsanız Firebase konsolunda Kimlik Doğrulama altında E-posta/Şifre ile oturum açma yöntemini etkinleştirdiğinizden emin olun.

d5225270159c040b.png

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, Firebase konsolu aracılığıyla manuel olarak yapılabilir, ancak temel bir Firestore yazımını göstermek için bunu uygulamanın kendisinde yapacağız.

Uygulamamızdaki ana model nesnesi bir restorandır. Firestore verileri belgelere, koleksiyonlara ve alt koleksiyonlara bölünmüştür. Her restoranı, restaurants adı verilen üst düzey bir koleksiyonda bir belge olarak saklayacağız. Firestore veri modeli hakkında daha fazla bilgi edinmek isterseniz belgelerdeki belgeler ve koleksiyonlar hakkında bilgi edinin.

Firestore'a veri ekleyebilmemiz için restoran koleksiyonuna referans almamız gerekiyor. RestaurantsTableViewController.didTapPopulateButton(_:) yöntemindeki iç for döngüsüne aşağıdakini 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 sonrasına şunu 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, restoran koleksiyonuna yeni bir belge ekler. Belge verileri, Restoran yapısından aldığımız bir sözlükten gelir.

Neredeyse bu noktaya geldik; Firestore'a belge yazabilmemiz için önce 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. Şimdilik yalnızca kimliği doğrulanmış kullanıcıların veritabanının tamamını okumasına ve yazmasına izin vereceğiz. Bu, bir üretim uygulaması için biraz fazla müsamahakârdır, ancak uygulama oluşturma süreci sırasında yeterince rahat bir şey istiyoruz, böylece denemeler sırasında sürekli kimlik doğrulama sorunlarıyla karşılaşmayacağız. Bu codelab'in sonunda güvenlik kurallarınızı nasıl sıkılaştıracağınız ve istenmeyen okuma ve yazma olasılığını nasıl sınırlandıracağınız hakkında konuşacağı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 atın.

Uygulamayı çalıştırın ve oturum açın. Ardından sol üstteki " Doldur " düğmesine dokunun; bu, bir dizi restoran belgesi oluşturacaktır, ancak bunu henüz uygulamada görmeyeceksiniz.

Ardından Firebase konsolundaki Firestore verileri sekmesine gidin. Artık restoran koleksiyonunda yeni girişler görmelisiniz:

Ekran Görüntüsü 2017-07-06, 12.45.38 PM.png

Tebrikler, az önce bir iOS uygulamasından Firestore'a veri yazdınız! Bir sonraki bölümde Firestore'dan verileri nasıl alacağınızı ve uygulamada nasıl görüntüleyeceğinizi öğreneceksiniz.

5. Firestore'dan Verileri Görüntüleyin

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, bir sorgu oluşturmak ve bir anlık görüntü dinleyicisi eklemektir. Bu dinleyici, sorguyla eşleşen tüm mevcut verilerden haberdar edilecek ve güncellemeleri gerçek zamanlı olarak alacaktır.

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

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

Bu sorgu, "restoranlar" adlı üst düzey koleksiyondaki en fazla 50 restoranı getirir. Artık bir sorgumuz olduğuna göre, verileri Firestore'dan uygulamamıza yüklemek için bir anlık görüntü dinleyicisi 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, koleksiyonu Firestore'dan indirir ve yerel olarak bir dizide saklar. addSnapshotListener(_:) çağrısı, sorguya, sunucudaki veriler her değiştiğinde görünüm denetleyicisini güncelleyecek bir anlık görüntü dinleyicisi ekler. Güncellemeleri otomatik olarak alıyoruz ve değişiklikleri manuel olarak iletmemize gerek yok. Unutmayın, bu anlık görüntü dinleyicisi, sunucu tarafındaki bir değişikliğin sonucu olarak herhangi bir zamanda çağrılabilir, dolayısıyla uygulamamızın değişiklikleri işleyebilmesi önemlidir.

Sözlüklerimizi yapılarla eşleştirdikten sonra (bkz. Restaurant.swift ), verileri görüntülemek yalnızca birkaç görünüm özelliği atamaktan ibarettir. RestaurantsTableViewController.swift dosyasındaki 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, önceki değer türleri koleksiyonunun ayrı ayrı tablo görünümü hücrelerine eşlenmesiyle ilgilenir.

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 olduğunu doğrulayın. Bu bölümü başarıyla tamamladıysanız uygulamanız artık Cloud Firestore ile veri okuyor ve yazıyor!

391c0259bf05ac25.png

6. Verileri Sıralama ve Filtreleme

Şu anda uygulamamız restoranların bir listesini gösteriyor 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 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 koleksiyonun üyelerini indirmesini sağlayacaktır. Bu durumda, yalnızca category "Dim Sum" olduğu restoranlar indirilecektir.

Bu uygulamada kullanıcı, "San Francisco'da Pizza" veya "Los Angeles'ta Popülerliğe göre sipariş edilen deniz ürünleri" gibi belirli sorgular oluşturmak için birden fazla filtreyi zincirleyebilir.

RestaurantsTableViewController.swift açın ve query(withCategory:city:price:sortBy:) 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 kod parçası, kullanıcı girişine dayalı olarak tek bir bileşik sorgu oluşturmak için birden fazla whereField ve order cümleciği ekler. Artık sorgumuz yalnızca kullanıcının gereksinimlerine uygun restoranları döndürecektir.

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 şuna benzeyen 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 dizinlere ihtiyaç duymasıdır. Sorgularda dizinlerin gerekli kılınması, Firestore'un geniş ölçekte hızlı olmasını sağlar. Hata mesajındaki bağlantının açılması, Firebase konsolunda dizin oluşturma kullanıcı arayüzünü doğru parametrelerin girildiği şekilde otomatik olarak açacaktır. Firestore'daki dizinler hakkında daha fazla bilgi edinmek için belgeleri ziyaret edin .

7. Bir işlemde veri yazmak

Bu bölümde kullanıcıların restoranlara yorum gönderme özelliğini ekleyeceğiz. Şu ana kadar tüm yazılarımız atomik ve nispeten basitti. Bunlardan herhangi biri hatalıysa, muhtemelen kullanıcıdan bunları yeniden denemesini veya otomatik olarak yeniden denemesini isteriz.

Bir restorana derecelendirme eklemek için birden fazla okuma ve yazmayı koordine etmemiz gerekir. Öncelikle yorumun kendisi gönderilmeli, ardından restoranın puan sayısı ve ortalama puanı güncellenmelidir. 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 gerçekleştirmemize olanak tanıyan işlem işlevselliği sağlayarak verilerimizin tutarlı kalmasını sağlar.

RestaurantDetailViewController.reviewController(_:didSubmitFormWithReview:) içindeki tüm izin 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 ele alınacaktır. Güncelleme sunucuda başarısız olursa, Firestore bunu birkaç kez otomatik olarak yeniden deneyecektir. Bu, hata durumumuzun 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 yetkisine sahip değilse.

8. Güvenlik kuralları

Uygulamamızın kullanıcıları veritabanımızdaki her veriyi okuyup yazamamalıdır. Örneğin, bir restoranın derecelendirmelerini herkes görebilmeli ancak yalnızca kimliği doğrulanmış bir kullanıcının derecelendirme yayınlamasına izin verilmelidir. İstemciye iyi kod yazmak yeterli değil, tamamen güvenli olması için veri güvenliği modelimizi arka uçta belirtmemiz gerekiyor. 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ına daha derinlemesine bakalım. Firebase konsolunu açın ve Firestore sekmesinde Veritabanı > Kurallar'a gidin.

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 global bir değişkendir ve eklediğimiz koşul, kullanıcıların herhangi bir şey 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şiklikler yapmak için Firestore API'sini kullanmasını engeller. Bu iyi bir başlangıç ​​ama Firestore kurallarını çok daha güçlü şeyler yapmak için kullanabiliriz.

İncelemenin kullanıcı kimliğinin kimliği doğrulanmış kullanıcının kimliğiyle eşleşmesi için inceleme yazmalarını kısıtlayalım. Bu, kullanıcıların birbirlerinin kimliğine bürünmelerini ve sahte incelemeler bırakamamalarını sağlar. Güvenlik kurallarınızı aşağıdakiyle 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 bildirimi, restaurants koleksiyonuna ait herhangi bir belgenin ratings adlı alt koleksiyonuyla eşleşir. allow write koşulu, incelemenin kullanıcı kimliğinin kullanıcının kimliğiyle eşleşmemesi durumunda herhangi bir incelemenin gönderilmesini engeller. İkinci eşleşme ifadesi, kimliği doğrulanmış herhangi bir kullanıcının restoranları veritabanına okumasına ve yazmasına olanak tanır.

Bu, incelemelerimiz için gerçekten işe yarıyor; çünkü güvenlik kurallarını, daha önce uygulamamıza yazdığımız, kullanıcıların yalnızca kendi incelemelerini yazabileceği yönündeki örtülü garantiyi açıkça belirtmek için kullandık. İncelemeler için bir düzenleme veya silme işlevi ekleseydik, aynı kurallar dizisi kullanıcıların diğer kullanıcıların incelemelerini değiştirmesini veya silmesini de engellerdi. Ancak Firestore kuralları, belgelerin tamamı yerine belgelerin içindeki bireysel alanlara yazmayı sınırlamak için daha ayrıntılı bir şekilde de kullanılabilir. Bunu, kullanıcıların bir restoranın yalnızca derecelendirmelerini, ortalama derecelendirmesini ve derecelendirme sayısını güncellemesine izin vermek için kullanabiliriz; böylece kötü niyetli bir kullanıcının bir restoranın adını veya konumunu değiştirme olasılığını ortadan kaldırırız.

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 yazma iznimizi oluşturma ve güncelleme olarak ayırdık, böylece hangi işlemlere izin verilmesi gerektiği konusunda daha spesifik olabiliriz. Herhangi bir kullanıcı, codelab'in başında yaptığımız Doldur butonunun işlevselliğini koruyarak restoranları veri tabanına yazabilir 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 aynı 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 ileri düzey okuma ve yazma işlemlerinin nasıl yapıldığını ve güvenlik kurallarıyla veri erişiminin nasıl güvence altına alınacağını öğrendiniz. Tam çözümü codelab-complete şubesinde bulabilirsiniz.

Firestore hakkında daha fazla bilgi edinmek için aşağıdaki kaynakları ziyaret edin: