Cloud Firestore iOS Codelab

Koleksiyonlar ile düzeninizi koruyun İçeriği tercihlerinize göre kaydedin ve kategorilere ayırın.

1. Genel Bakış

Hedefler

Bu kod laboratuvarında, iOS'ta Swift'te Firestore destekli bir restoran tavsiyesi uygulaması oluşturacaksınız. Şunları nasıl yapacağınızı öğ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ı ve güvenlik kurallarını kullanın
  4. Karmaşık Firestore sorguları yazın

Önkoşullar

Bu kod laboratuvarına başlamadan önce şunları kurduğunuzdan emin olun:

  • Xcode sürüm 13.0 (veya üstü)
  • CocoaPods 1.11.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 projenizi "Firestore iOS Codelab" olarak adlandırın.

3. Örnek Projeyi Alın

Kodu İndir

Ö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

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

Firebase'i kurun

Yeni bir Firestore projesi oluşturmak için belgeleri takip edin. Projenizi aldıktan sonra, projenizin GoogleService-Info.plist dosyasını Firebase konsolundan indirin ve Xcode projesinin kök dizinine sürükleyin. Uygulamanın doğru şekilde yapılandırıldığından ve başlatıldığında artık çökmediğ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. Oturum açamıyorsanız, Kimlik Doğrulama altında Firebase konsolunda E-posta/Parola oturum açma yöntemini etkinleştirdiğinizden emin olun.

d5225270159c040b.png

4. Firestore'a Veri Yazın

Bu bölümde, uygulama kullanıcı arayüzünü doldurabilmemiz için Firestore'a bazı veriler yazacağız. Bu, Firebase konsolu aracılığıyla manuel olarak yapılabilir, ancak temel bir Firestore yazısını göstermek için 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 okuyun.

Firestore'a veri ekleyebilmemiz için önce restoran koleksiyonuna bir referans almamız gerekiyor. 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. Son eklediğimiz 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, restoranlar koleksiyonuna yeni bir belge ekler. Belge verileri, bir Restaurant yapısından aldığımız bir sözlükten gelir.

Neredeyse geldik – 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. Şimdilik, yalnızca kimliği doğrulanmış kullanıcıların tüm veritabanını okuyup yazmasına izin vereceğiz. Bu, bir üretim uygulaması için biraz fazla müsamahakar, ancak uygulama oluşturma sürecinde, deney yaparken sürekli olarak kimlik doğrulama sorunlarıyla karşılaşmamak için yeterince rahat bir şey istiyoruz. Bu kod laboratuvarının sonunda, güvenlik kurallarınızı nasıl katılaştıracağınız ve istenmeyen okuma ve yazma olasılıkları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 bakın.

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

Ardından, Firebase konsolunda Firestore veri 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 nasıl veri 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 uygulamada nasıl görüntüleyeceğinizi öğreneceksiniz. İki temel 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.

İlk önce, varsayılan, filtrelenmemiş restoran listesine hizmet edecek sorguyu oluşturalım. RestaurantsTableViewController.baseQuery() uygulaması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, Firestore'dan uygulamamıza veri 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 depolar. 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 zorlamamız gerekmiyor. 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, bu nedenle uygulamamızın değişiklikleri işleyebilmesi önemlidir.

Sözlüklerimizi yapılarla eşledikten sonra (bkz. Restaurant.swift ), verileri görüntülemek yalnızca birkaç görünüm özelliği atama meselesidir. RestoranTableViewController.swift içindeki RestaurantsTableViewController.swift RestaurantTableViewCell.populate(restaurant:) 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, önceden değer türleri koleksiyonunun tek tek tablo görünümü hücrelerine eşlenmesini sağlayan tableView(_:cellForRowAtIndexPath:) yönteminden çağrılır.

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 okuyor ve yazıyor!

391c0259bf05ac25.png

6. Verileri Sıralama ve Filtreleme

Şu anda uygulamamız bir restoran listesi 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.

İşte 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ıtlamalara uyan koleksiyonun üyelerini indirmesini sağlar. Bu durumda, yalnızca category "Dim Sum" olan restoranları indirir.

Bu uygulamada kullanıcı, "San Francisco'da Pizza" veya "Los Angeles'ta Popülerliğe Göre Sıralanan Deniz Ürünleri" gibi belirli sorgular oluşturmak için birden çok 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 snippet, kullanıcı girişine dayalı olarak tek bir bileşik sorgu oluşturmak için birden çok whereField ve order yan tümcesi ekler. Artık sorgumuz yalnızca kullanıcının gereksinimleriyle eşleşen restoranları döndürecektir.

Projenizi çalıştırın ve fiyat, şehir ve kategoriye göre filtre uygulayabildiğinizi doğrulayın (kategori ve şehir adlarını tam olarak yazdığınızdan emin olun). Test ederken, 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 dizin gerektirmesidir. Sorgularda dizinlerin zorunlu kılınması, Firestore'un geniş ölçekte hızlı olmasını sağlar. Hata mesajındaki bağlantı açıldığında, Firebase konsolunda dizin oluşturma kullanıcı arabirimi otomatik olarak doğru parametrelerle birlikte açılır. Firestore'daki dizinler hakkında daha fazla bilgi edinmek için belgeleri ziyaret edin .

7. Bir işlemde veri yazma

Bu bölümde, kullanıcıların restoranlara yorum gönderme olanağını ekleyeceğiz. Şimdiye kadar, tüm yazılarımız atomik ve nispeten basitti. Bunlardan herhangi biri hatalıysa, büyük olasılıkla kullanıcıdan bunları yeniden denemesini veya otomatik olarak yeniden denemesini isteriz.

Bir restorana derecelendirme eklemek için birden çok okuma ve yazma işlemini koordine etmemiz gerekir. Öncelikle incelemenin kendisi gönderilmeli ve 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 başka bir bölümdeki verilerle eşleşmediği tutarsız bir durumda kalırız.

Neyse ki Firestore, tek bir atomik işlemde birden çok okuma ve yazma gerçekleştirmemize izin vererek verilerimizin tutarlı kalmasını sağlayan işlem işlevselliği sağlar.

RestaurantDetailViewController.reviewController(_:didSubmitFormWithReview:) içindeki 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 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 otomatik olarak birkaç kez 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ının yazmaya çalıştığı yola yazma yetkisi yoksa.

8. Güvenlik kuralları

Uygulamamızın kullanıcıları, veri tabanımızdaki her veri parçasını okuyup yazamamalıdır. Örneğin, bir restoranın puanlarını herkes görebilmeli, ancak yalnızca kimliği doğrulanmış bir kullanıcının puan vermesine izin verilmelidir. İstemcide iyi kod yazmak yeterli değil, tamamen güvenli olması için arka uçta veri güvenlik modelimizi 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 yakından 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 kurallardaki request değişkeni, tüm kurallarda bulunan genel 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'yi kullanmasını engeller. Bu iyi bir başlangıç, ancak çok daha güçlü şeyler yapmak için Firestore kurallarını kullanabiliriz.

İnceleme kullanıcı kimliğinin, kimliği doğrulanmış kullanıcının kimliğiyle eşleşmesi için inceleme yazma işlemlerini kısıtlayalım. Bu, kullanıcıların birbirlerinin kimliğine bürünmelerini ve sahte incelemeler bırakmamalarını sağlar. 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 bildirimi, restaurants koleksiyonuna ait herhangi bir belgenin ratings adlı alt koleksiyonuyla eşleşir. allow write koşulu, incelemenin kullanıcı kimliği kullanıcınınkiyle eşleşmiyorsa 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 izin verir.

Bu, incelemelerimiz için gerçekten işe yarıyor, çünkü güvenlik kurallarını daha önce uygulamamıza yazdığımız zımni garantiyi açıkça belirtmek için kullandık - kullanıcıların yalnızca kendi incelemelerini yazabilecekleri. İncelemeler için bir düzenleme veya silme işlevi ekleseydik, bu tamamen aynı kurallar dizisi, kullanıcıların diğer kullanıcıların incelemelerini de değiştirmesini veya silmesini de engellerdi. Ancak Firestore kuralları, tüm belgeler yerine belgelerdeki tek tek 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 puanlarını, ortalama puanını ve puan sayısını güncellemesine izin vererek, kötü niyetli bir kullanıcının bir restoran adını veya konumunu değiştirme olasılığını ortadan kaldırmak için 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;
    }
  }
}

Burada yazma iznimizi oluşturma ve güncelleme olarak ayırdık, böylece hangi işlemlere izin verilmesi gerektiği konusunda daha net olabiliriz. Herhangi bir kullanıcı, kod laboratuvarının başında yaptığımız Doldur düğmesinin işlevselliğini koruyarak veritabanına restoranlar 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ında halihazırda var olan 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 bakın.

9. Sonuç

Bu kod laboratuvarında, 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. Tam çözümü codelab-complete dalında bulabilirsiniz.

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