Cloud Firestore verilerini Swift Codable ile eşleme

Swift 4'te kullanıma sunulan Swift Codable API, derleyicinin gücünden yararlanarak seri hale getirilmiş biçimlerdeki verilerin Swift türlerine eşlenmesini kolaylaştırır.

Bir web API'sindeki verileri uygulamanızın veri modeline (veya tam tersi) eşlemek için Codable'ı kullanmış olabilirsiniz ancak Codable, bu işlevden çok daha esnektir.

Bu kılavuzda, Codable'ın Cloud Firestore verilerini Swift türlerine (veya tam tersi) eşlemek için nasıl kullanılabileceğini inceleyeceğiz.

Cloud Firestore adresinden bir doküman getirirken uygulamanız anahtar/değer çiftlerinden oluşan bir sözlük (veya birden fazla doküman döndüren işlemlerden birini kullanıyorsanız sözlük dizisi) alır.

Artık Swift'te sözlükleri doğrudan kullanmaya devam edebilirsiniz. Bu sözlükler, kullanım alanınızın gerektirdiği esnekliği sunar. Ancak bu yaklaşım tür açısından güvenli değildir ve özellik adlarını yanlış yazarak veya ekibinizin geçen hafta heyecan verici yeni özelliği kullanıma sunduğunda eklediği yeni özelliği eşlemeyi unutarak izi zor bulunan hatalar eklemek kolaydır.

Geçmişte birçok geliştirici, sözlükleri Swift türleriyle eşlemelerine olanak tanıyan basit bir eşleme katmanı uygulayarak bu eksiklikleri gidermeye çalıştı. Ancak yine de bu uygulamaların çoğu, Cloud Firestore belgeleri ile uygulamanızın veri modelinin karşılık gelen türleri arasındaki eşlemenin manuel olarak belirtilmesine dayanır.

Cloud Firestore'ın Swift'in Codable API'si için verdiği destek sayesinde bu işlem çok daha kolay hale geliyor:

  • Artık herhangi bir eşleme kodunu manuel olarak uygulamanız gerekmeyecek.
  • Farklı adlara sahip özelliklerin nasıl eşleneceğini kolayca tanımlayabilirsiniz.
  • Swift'in birçok türü için yerleşik destek sunar.
  • Ayrıca, özel türlerin eşlenmesi için destek eklemek de kolaydır.
  • En önemlisi, basit veri modelleri için herhangi bir eşleme kodu yazmanız gerekmez.

Eşleme verileri

Cloud Firestore, anahtarları değerlerle eşleyen belgelerde veri depolar. Tek bir dokümandaki verileri getirmek için DocumentSnapshot.data() işlevini çağırabiliriz. Bu işlev, alan adlarını Any ile eşleyen bir sözlük döndürür: func data() -> [String : Any]?.

Bu nedenle, her bir alana erişmek için Swift'in alt simge söz dizimini kullanabiliriz.

import FirebaseFirestore

#warning("DO NOT MAP YOUR DOCUMENTS MANUALLY. USE CODABLE INSTEAD.")
func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)

  docRef.getDocument { document, error in
    if let error = error as NSError? {
      self.errorMessage = "Error getting document: \(error.localizedDescription)"
    }
    else {
      if let document = document {
        let id = document.documentID
        let data = document.data()
        let title = data?["title"] as? String ?? ""
        let numberOfPages = data?["numberOfPages"] as? Int ?? 0
        let author = data?["author"] as? String ?? ""
        self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author)
      }
    }
  }
}

Bu kod basit ve uygulaması kolay gibi görünse de aslında hassastır, bakımı zordur ve hatalara açıktır.

Gördüğünüz gibi, doküman alanlarının veri türleriyle ilgili varsayımlarda bulunuyoruz. Bunlar doğru olabilir veya olmayabilir.

Şema olmadığı için koleksiyona kolayca yeni bir belge ekleyebilir ve bir alan için farklı bir tür seçebilirsiniz. numberOfPages alanı için yanlışlıkla dize seçebilirsiniz. Bu durumda, bulunması zor bir eşleme sorunu ortaya çıkar. Ayrıca, yeni bir alan eklendiğinde eşleme kodunuzu güncellemeniz gerekir. Bu da oldukça zahmetlidir.

Ayrıca, Book özelliklerinin her biri için doğru türü tam olarak bilen Swift'in güçlü tür sisteminden yararlanmadığımızı da unutmayalım.

Codable nedir?

Apple'ın dokümanlarına göre Codable, "kendisini harici bir temsile dönüştürebilen ve harici bir temsilden dönüştürebilen bir türdür". Aslında Codable, Encodable ve Decodable protokolleri için bir tür takma adıdır. Derleyici, bir Swift türünü bu protokole uygun hale getirerek bu türün bir örneğini JSON gibi serileştirilmiş bir biçimden kodlamak/kodunu çözmek için gereken kodu sentezler.

Bir kitapla ilgili verileri depolamak için kullanılan basit bir tür şu şekilde görünebilir:

struct Book: Codable {
  var title: String
  var numberOfPages: Int
  var author: String
}

Gördüğünüz gibi, türü Codable'a uygun hale getirmek için çok az değişiklik yapılması gerekiyor. Yalnızca protokole uygunluk eklememiz gerekti. Başka değişiklik yapılması gerekmedi.

Bu işlem tamamlandıktan sonra artık bir kitabı kolayca JSON nesnesine kodlayabiliriz:

do {
  let book = Book(title: "The Hitchhiker's Guide to the Galaxy",
                  numberOfPages: 816,
                  author: "Douglas Adams")
  let encoder = JSONEncoder()
  let data = try encoder.encode(book)
} 
catch {
  print("Error when trying to encode book: \(error)")
}

Bir JSON nesnesinin Book örneğine kodunun çözülmesi şu şekilde çalışır:

let decoder = JSONDecoder()
let data = /* fetch data from the network */
let decodedBook = try decoder.decode(Book.self, from: data)

Codable kullanarak Cloud Firestore belgelerindeki
basit türlerle eşleme yapma

Cloud Firestore, basit dizelerden iç içe yerleştirilmiş haritalara kadar geniş bir veri türü grubunu destekler. Bunların çoğu doğrudan Swift'in yerleşik türlerine karşılık gelir. Daha karmaşık veri türlerine geçmeden önce bazı basit veri türlerinin nasıl eşlendiğine göz atalım.

Cloud Firestore belgelerini Swift türleriyle eşlemek için aşağıdaki adımları uygulayın:

  1. Projenize FirebaseFirestore çerçevesini eklediğinizden emin olun. Bunu yapmak için Swift Package Manager veya CocoaPods'u kullanabilirsiniz.
  2. FirebaseFirestore dosyasını Swift dosyanıza aktarın.
  3. Türünüzü Codable ile uyumlu hale getirin.
  4. (İsteğe bağlı, türü List görünümünde kullanmak istiyorsanız) Türünüze bir id özellik ekleyin ve Cloud Firestore'e bunu doküman kimliğiyle eşlemesini söylemek için @DocumentID kullanın. Bu konuyu aşağıda daha ayrıntılı olarak ele alacağız.
  5. Bir doküman referansını Swift türüyle eşlemek için documentReference.data(as: ) kullanın.
  6. Verileri Swift türlerinden bir documentReference.setData(from: ) belgesine eşlemek için Cloud Firestore kullanın.
  7. (İsteğe bağlıdır ancak kesinlikle önerilir) Uygun hata işlemeyi uygulayın.

Book türümüzü buna göre güncelleyelim:

struct Book: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
}

Bu tür zaten kodlanabilir olduğundan yalnızca id özelliğini eklememiz ve @DocumentID özellik sarmalayıcısıyla ek açıklamalar eklememiz gerekiyordu.

Bir dokümanı getirmek ve eşlemek için kullanılan önceki kod snippet'ini kullanarak tüm manuel eşleme kodunu tek bir satırla değiştirebiliriz:

func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)

  docRef.getDocument { document, error in
    if let error = error as NSError? {
      self.errorMessage = "Error getting document: \(error.localizedDescription)"
    }
    else {
      if let document = document {
        do {
          self.book = try document.data(as: Book.self)
        }
        catch {
          print(error)
        }
      }
    }
  }
}

getDocument(as:) işlevini çağırırken belge türünü belirterek bunu daha da kısa bir şekilde yazabilirsiniz. Bu işlem, eşlemeyi sizin için gerçekleştirir ve eşlenen belgeyi içeren bir Result türü ya da kod çözme işlemi başarısız olursa bir hata döndürür:

private func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)
  
  docRef.getDocument(as: Book.self) { result in
    switch result {
    case .success(let book):
      // A Book value was successfully initialized from the DocumentSnapshot.
      self.book = book
      self.errorMessage = nil
    case .failure(let error):
      // A Book value could not be initialized from the DocumentSnapshot.
      self.errorMessage = "Error decoding document: \(error.localizedDescription)"
    }
  }
}

Mevcut bir dokümanı güncellemek için documentReference.setData(from: ) işlevini çağırmanız yeterlidir. Temel hata işleme de dahil olmak üzere Book örneğini kaydetme kodu aşağıda verilmiştir:

func updateBook(book: Book) {
  if let id = book.id {
    let docRef = db.collection("books").document(id)
    do {
      try docRef.setData(from: book)
    }
    catch {
      print(error)
    }
  }
}

Yeni bir belge eklerken Cloud Firestore, belgeye yeni bir belge kimliği atama işlemini otomatik olarak gerçekleştirir. Bu özellik, uygulama çevrimdışı olduğunda bile çalışır.

func addBook(book: Book) {
  let collectionRef = db.collection("books")
  do {
    let newDocReference = try collectionRef.addDocument(from: self.book)
    print("Book stored with new document reference: \(newDocReference)")
  }
  catch {
    print(error)
  }
}

Cloud Firestore, basit veri türlerini eşlemenin yanı sıra bir dizi başka veri türünü de destekler. Bunlardan bazıları, bir belgenin içinde iç içe yerleştirilmiş nesneler oluşturmak için kullanabileceğiniz yapılandırılmış türlerdir.

İç içe yerleştirilmiş özel türler

Belgelerimizde eşlemek istediğimiz özelliklerin çoğu, kitabın başlığı veya yazarın adı gibi basit değerlerdir. Peki daha karmaşık bir nesneyi depolamamız gereken durumlarda ne yapmalıyız? Örneğin, kitabın kapağının URL'lerini farklı çözünürlüklerde saklamak isteyebiliriz.

Cloud Firestore içinde bunu yapmanın en kolay yolu harita kullanmaktır:

İç içe yerleştirilmiş özel türü Firestore belgesinde depolama

İlgili Swift yapısını yazarken Cloud Firestore öğesinin URL'leri desteklemesinden yararlanabiliriz. URL içeren bir alan depolanırken bu alan dizeye dönüştürülür ve bunun tersi de geçerlidir:

struct CoverImages: Codable {
  var small: URL
  var medium: URL
  var large: URL
}

struct BookWithCoverImages: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var cover: CoverImages?
}

Cloud Firestore dokümanında kapak haritası için CoverImages yapısını nasıl tanımladığımıza dikkat edin. BookWithCoverImages üzerindeki kapak özelliğini isteğe bağlı olarak işaretleyerek bazı belgelerin kapak özelliği içermeyebileceği gerçeğini ele alabiliyoruz.

Veri getirme veya güncelleme için neden kod snippet'i olmadığını merak ediyorsanız Cloud Firestore'dan okuma ya da Cloud Firestore'ya yazma için kodu ayarlamanıza gerek olmadığını öğrenmek sizi memnun edecektir. Tüm bunlar, ilk bölümde yazdığımız kodla çalışır.

Diziler

Bazen bir dokümanda değerler koleksiyonu depolamak isteriz. Kitap türleri iyi bir örnektir: Otostopçunun Galaksi Rehberi gibi bir kitap birkaç kategoriye (bu örnekte "Bilim Kurgu" ve "Komedi") girebilir:

Firestore belgesinde dizi depolama

Cloud Firestore içinde bunu bir değer dizisi kullanarak modelleyebiliriz. Bu, kodlanabilir tüm türler (ör. String, Int vb.) için desteklenir. Aşağıdaki örnekte, Book modelimize nasıl tür dizisi ekleneceği gösterilmektedir:

public struct BookWithGenre: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var genres: [String]
}

Bu yöntem, kodlanabilir tüm türlerde çalıştığı için özel türleri de kullanabiliriz. Her kitap için bir etiket listesi saklamak istediğimizi varsayalım. Etiketin adının yanı sıra rengini de aşağıdaki gibi saklamak istiyoruz:

Firestore belgesinde özel tür dizisi depolama

Etiketleri bu şekilde depolamak için yapmamız gereken tek şey, bir etiketi temsil etmek üzere Tag yapısını uygulamak ve bunu kodlanabilir hale getirmektir:

struct Tag: Codable, Hashable {
  var title: String
  var color: String
}

Bu şekilde, Tags dizisini Book dokümanlarımızda saklayabiliriz.

struct BookWithTags: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var tags: [Tag]
}

Doküman kimliklerini eşleme hakkında kısa bilgi

Diğer türleri eşlemeye geçmeden önce belge kimliklerini eşleme hakkında konuşalım.

@DocumentID özelliğini, önceki örneklerin bazılarında Cloud Firestore belgelerimizin doküman kimliğini Swift türlerimizin id özelliğiyle eşlemek için kullandık. Bu durumun birkaç önemli nedeni vardır:

  • Kullanıcı yerel değişiklikler yaptığında hangi dokümanın güncelleneceğini bilmemize yardımcı olur.
  • SwiftUI'ın List öğesi, öğelerin eklenirken yer değiştirmesini önlemek için öğelerinin Identifiable olmasını gerektirir.

@DocumentID olarak işaretlenen bir özelliğin, doküman geri yazılırken Cloud Firestore kodlayıcısı tarafından kodlanmayacağını belirtmekte fayda var. Bunun nedeni, doküman kimliğinin dokümanın kendisinin bir özelliği olmamasıdır. Bu nedenle, dokümana yazılması bir hata olur.

İç içe yerleştirilmiş türlerle (ör. bu kılavuzdaki önceki bir örnekte Book üzerindeki etiket dizisi) çalışırken @DocumentID özelliği eklemeniz gerekmez: İç içe yerleştirilmiş özellikler, Cloud Firestore dokümanının bir parçasıdır ve ayrı bir doküman oluşturmaz. Bu nedenle, doküman kimliğine ihtiyaç duymazlar.

Tarihler ve saatler

Cloud Firestore, tarih ve saatleri işlemek için yerleşik bir veri türüne sahiptir ve Cloud Firestore'nın Codable desteği sayesinde bu türleri kullanmak kolaydır.

Tüm programlama dillerinin anası olarak kabul edilen ve 1843'te icat edilen Ada'yı temsil eden bu belgeye göz atalım:

Tarihleri Firestore belgesinde depolama

Bu belgeyi eşlemek için kullanılan Swift türü şu şekilde görünebilir:

struct ProgrammingLanguage: Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
}

Tarihler ve saatlerle ilgili bu bölümü @ServerTimestamp hakkında konuşmadan geçemeyiz. Bu özellik sarmalayıcı, uygulamanızdaki zaman damgalarıyla ilgilenme konusunda oldukça güçlüdür.

Herhangi bir dağıtılmış sistemde, tek tek sistemlerdeki saatlerin her zaman tamamen senkronize olmaması olasıdır. Bunun büyük bir sorun olmadığını düşünebilirsiniz ancak biraz senkronize olmayan bir saatin borsa ticaret sistemi üzerindeki etkilerini düşünün: Bir ticareti gerçekleştirirken milisaniyelik bir sapma bile milyonlarca dolarlık bir farka neden olabilir.

Cloud Firestore, @ServerTimestamp ile işaretlenmiş özellikleri aşağıdaki şekilde işler: Özellik, siz depoladığınızda (örneğin, addDocument() kullanarak) nil ise Cloud Firestore, alanı veritabanına yazıldığı sırada geçerli sunucu zaman damgasıyla doldurur. Alan, nil addDocument() veya updateData()'ı çağırdığınızda Cloud Firestore değilse Cloud Firestore, özellik değerine dokunmaz. Bu sayede, createdAt ve lastUpdatedAt gibi alanları kolayca uygulayabilirsiniz.

Geopoints

Uygulamalarımızda coğrafi konumlar her yerde bulunur. Bu verileri depolayarak birçok heyecan verici özellikten yararlanabilirsiniz. Örneğin, uygulamanız bir hedefe ulaştığınızda görevi size hatırlatabilmesi için bir görevin konumunu depolamak faydalı olabilir.

Cloud Firestore, herhangi bir konumun boylam ve enlemini depolayabilen yerleşik bir veri türü olan GeoPoint'ye sahiptir. Bir Cloud Firestore dokümanındaki konumları eşlemek için GeoPoint türünü kullanabiliriz:

struct Office: Codable {
  @DocumentID var id: String?
  var name: String
  var location: GeoPoint
}

Swift'teki karşılık gelen tür CLLocationCoordinate2D'dır ve bu iki tür arasında aşağıdaki işlemle eşleme yapabiliriz:

CLLocationCoordinate2D(latitude: office.location.latitude,
                      longitude: office.location.longitude)

Belgeleri fiziksel konuma göre sorgulama hakkında daha fazla bilgi edinmek için bu çözüm kılavuzuna göz atın.

Sıralamalar

Numaralandırmalar, Swift'teki en az değer verilen dil özelliklerinden biridir. Göründüğünden çok daha fazlasını sunar. Numaralandırılmış türlerin yaygın kullanım alanlarından biri, bir şeyin ayrı durumlarını modellemektir. Örneğin, makaleleri yönetmek için bir uygulama yazıyor olabiliriz. Bir makalenin durumunu izlemek için Status enum'ını kullanabiliriz:

enum Status: String, Codable {
  case draft
  case inReview
  case approved
  case published
}

Cloud Firestore, numaralandırmaları yerel olarak desteklemez (yani değer kümesini zorlayamaz) ancak numaralandırmaların türü belirlenebilir ve kodlanabilir bir tür seçebiliriz. Bu örnekte String seçilmiştir. Bu, Cloud Firestore dokümanında depolanırken tüm enum değerlerinin dizeye/dizeden eşleneceği anlamına gelir.

Ayrıca Swift, özel ham değerleri desteklediğinden hangi değerlerin hangi enum durumuna karşılık geldiğini bile özelleştirebiliriz. Örneğin, Status.inReview durumu "inceleniyor" olarak saklamaya karar verirsek yukarıdaki enum'ı şu şekilde güncelleyebiliriz:

enum Status: String, Codable {
  case draft
  case inReview = "in review"
  case approved
  case published
}

Eşlemeyi özelleştirme

Bazen, eşlemek istediğimiz Cloud Firestore belgelerinin özellik adları, Swift'teki veri modelimizdeki özelliklerin adlarıyla eşleşmez. Örneğin, iş arkadaşlarımızdan biri Python geliştiricisi olabilir ve tüm özellik adları için snake_case'i seçmeye karar vermiş olabilir.

Endişelenmeyin: Codable bu konuda bize yardımcı oluyor.

Bu gibi durumlarda CodingKeys kullanabiliriz. Bu, belirli özelliklerin nasıl eşleneceğini belirtmek için kodlanabilir bir yapıya ekleyebileceğimiz bir enum'dur.

Şu belgeyi inceleyin:

snake_case biçiminde özellik adına sahip bir Firestore dokümanı

Bu belgeyi, String türünde bir ad özelliğine sahip bir yapıya eşlemek için ProgrammingLanguage yapısına bir CodingKeys enum eklememiz ve belgedeki özelliğin adını belirtmemiz gerekir:

struct ProgrammingLanguage: Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
  
  enum CodingKeys: String, CodingKey {
    case id
    case name = "language_name"
    case year
  }
}

Codable API, varsayılan olarak eşlemeye çalıştığımız Cloud Firestore belgelerindeki özellik adlarını belirlemek için Swift türlerimizin özellik adlarını kullanır. Bu nedenle, özellik adları eşleştiği sürece kodlanabilir türlerimize CodingKeys eklemenize gerek yoktur. Ancak belirli bir tür için CodingKeys kullandığımızda, eşlemek istediğimiz tüm özellik adlarını eklememiz gerekir.

Yukarıdaki kod snippet'inde, SwiftUI List görünümünde tanımlayıcı olarak kullanmak isteyebileceğimiz bir id özelliği tanımladık. CodingKeys içinde belirtmediğimiz bir özellik, veri getirilirken eşlenmez ve bu nedenle nil olur. Bu durumda, List görünümü ilk belgeyle doldurulur.

İlgili CodingKeys enum'unda vaka olarak listelenmeyen tüm özellikler eşleme işlemi sırasında yoksayılır. Bu, bazı mülklerin özellikle eşlenmesini istemiyorsak kullanışlı olabilir.

Örneğin, reasonWhyILoveThis mülkünün eşlenmesini istemiyorsak tek yapmamız gereken bu mülkü CodingKeys enum'undan kaldırmaktır:

struct ProgrammingLanguage: Identifiable, Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
  var reasonWhyILoveThis: String = ""
  
  enum CodingKeys: String, CodingKey {
    case id
    case name = "language_name"
    case year
  }
}

Bazen boş bir özelliği Cloud Firestore dokümanına geri yazmak isteyebiliriz. Swift'te değerin olmadığını belirtmek için isteğe bağlı değerler kavramı vardır ve Cloud Firestore da null değerlerini destekler. Ancak nil değeri olan isteğe bağlı öğelerin kodlanmasıyla ilgili varsayılan davranış, bu öğeleri atlamaktır. @ExplicitNull, Swift isteğe bağlı değerlerinin kodlanırken nasıl işleneceği konusunda bize biraz kontrol sağlar: İsteğe bağlı bir özelliği @ExplicitNull olarak işaretleyerek Cloud Firestore'e, nil değeri içeriyorsa bu özelliği belgeye boş değerle yazmasını söyleyebiliriz.

Renkleri eşlemek için özel kodlayıcı ve kod çözücü kullanma

Codable ile verileri eşleme konusunu ele aldığımız bu yazının son bölümünde özel kodlayıcıları ve kod çözücüleri tanıtacağız. Bu bölümde yerel bir Cloud Firestore veri türü ele alınmamaktadır ancak özel kodlayıcılar ve kod çözücüler Cloud Firestore uygulamalarınızda oldukça faydalıdır.

"Renkleri nasıl eşleyebilirim?" sorusu, geliştiricilerin en sık sorduğu sorulardan biridir. Bu soru yalnızca Cloud Firestore için değil, Swift ile JSON arasındaki eşleme için de geçerlidir. Piyasada birçok çözüm olsa da çoğu JSON'a odaklanır ve neredeyse tamamı renkleri RGB bileşenlerinden oluşan iç içe yerleştirilmiş bir sözlük olarak eşler.

Daha iyi ve daha basit bir çözüm olması gerekir. Neden web renklerini (veya daha spesifik olarak CSS onaltılık renk gösterimini) kullanmıyoruz? Bunların kullanımı kolaydır (temelde yalnızca bir dize) ve şeffaflığı bile destekler.

Bir Swift Color öğesini onaltılık değerine eşleyebilmek için Color öğesine Codable ekleyen bir Swift uzantısı oluşturmamız gerekir.

extension Color {

 init(hex: String) {
    let rgba = hex.toRGBA()

    self.init(.sRGB,
              red: Double(rgba.r),
              green: Double(rgba.g),
              blue: Double(rgba.b),
              opacity: Double(rgba.alpha))
    }

    //... (code for translating between hex and RGBA omitted for brevity)

}

extension Color: Codable {
  
  public init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let hex = try container.decode(String.self)

    self.init(hex: hex)
  }
  
  public func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    try container.encode(toHex)
  }

}

decoder.singleValueContainer() kullanarak, RGBA bileşenlerini iç içe yerleştirmek zorunda kalmadan bir String değerini Color eşdeğerine çözebiliriz. Ayrıca, bu değerleri önce dönüştürmenize gerek kalmadan uygulamanızın web kullanıcı arayüzünde kullanabilirsiniz.

Bu sayede, etiketleri eşlemek için kodu güncelleyebiliriz. Böylece, etiket renklerini uygulamamızın kullanıcı arayüzü kodunda manuel olarak eşlemek yerine doğrudan işlemek daha kolay olur:

struct Tag: Codable, Hashable {
  var title: String
  var color: Color
}

struct BookWithTags: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var tags: [Tag]
}

Hataları işleme

Yukarıdaki kod snippet'lerinde hata işlemeyi kasıtlı olarak minimum düzeyde tuttuk ancak bir üretim uygulamasında hataları düzgün bir şekilde işlemeyi unutmayın.

Karşılaşabileceğiniz tüm hata durumlarının nasıl ele alınacağını gösteren bir kod snippet'i aşağıda verilmiştir:

class MappingSimpleTypesViewModel: ObservableObject {
  @Published var book: Book = .empty
  @Published var errorMessage: String?
  
  private var db = Firestore.firestore()
  
  func fetchAndMap() {
    fetchBook(documentId: "hitchhiker")
  }
  
  func fetchAndMapNonExisting() {
    fetchBook(documentId: "does-not-exist")
  }
  
  func fetchAndTryMappingInvalidData() {
    fetchBook(documentId: "invalid-data")
  }
  
  private func fetchBook(documentId: String) {
    let docRef = db.collection("books").document(documentId)
    
    docRef.getDocument(as: Book.self) { result in
      switch result {
      case .success(let book):
        // A Book value was successfully initialized from the DocumentSnapshot.
        self.book = book
        self.errorMessage = nil
      case .failure(let error):
        // A Book value could not be initialized from the DocumentSnapshot.
        switch error {
        case DecodingError.typeMismatch(_, let context):
          self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
        case DecodingError.valueNotFound(_, let context):
          self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
        case DecodingError.keyNotFound(_, let context):
          self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
        case DecodingError.dataCorrupted(let key):
          self.errorMessage = "\(error.localizedDescription): \(key)"
        default:
          self.errorMessage = "Error decoding document: \(error.localizedDescription)"
        }
      }
    }
  }
}

Canlı güncellemelerdeki hataları ele alma

Önceki kod snippet'i, tek bir doküman getirilirken hataların nasıl işleneceğini gösterir. Cloud Firestore, verileri yalnızca bir kez getirmenin yanı sıra anlık görüntü dinleyicileri adı verilen bir yöntemle güncellemeleri gerçekleşir gerçekleşmez uygulamanıza göndermeyi de destekler. Bir koleksiyona (veya sorguya) anlık görüntü dinleyicisi kaydedebiliriz ve Cloud Firestore, güncelleme olduğunda dinleyicimizi çağırır.

Aşağıda, anlık görüntü dinleyicisinin nasıl kaydedileceğini, Codable kullanarak verilerin nasıl eşleneceğini ve oluşabilecek hataların nasıl işleneceğini gösteren bir kod snippet'i verilmiştir. Ayrıca, koleksiyona yeni bir belgenin nasıl ekleneceği de gösterilir. Gördüğünüz gibi, eşlenen dokümanları tutan yerel diziyi kendimiz güncellememize gerek yoktur. Bu işlem, anlık görüntü dinleyicisindeki kod tarafından gerçekleştirilir.

class MappingColorsViewModel: ObservableObject {
  @Published var colorEntries = [ColorEntry]()
  @Published var newColor = ColorEntry.empty
  @Published var errorMessage: String?
  
  private var db = Firestore.firestore()
  private var listenerRegistration: ListenerRegistration?
  
  public func unsubscribe() {
    if listenerRegistration != nil {
      listenerRegistration?.remove()
      listenerRegistration = nil
    }
  }
  
  func subscribe() {
    if listenerRegistration == nil {
      listenerRegistration = db.collection("colors")
        .addSnapshotListener { [weak self] (querySnapshot, error) in
          guard let documents = querySnapshot?.documents else {
            self?.errorMessage = "No documents in 'colors' collection"
            return
          }
          
          self?.colorEntries = documents.compactMap { queryDocumentSnapshot in
            let result = Result { try queryDocumentSnapshot.data(as: ColorEntry.self) }
            
            switch result {
            case .success(let colorEntry):
              if let colorEntry = colorEntry {
                // A ColorEntry value was successfully initialized from the DocumentSnapshot.
                self?.errorMessage = nil
                return colorEntry
              }
              else {
                // A nil value was successfully initialized from the DocumentSnapshot,
                // or the DocumentSnapshot was nil.
                self?.errorMessage = "Document doesn't exist."
                return nil
              }
            case .failure(let error):
              // A ColorEntry value could not be initialized from the DocumentSnapshot.
              switch error {
              case DecodingError.typeMismatch(_, let context):
                self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
              case DecodingError.valueNotFound(_, let context):
                self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
              case DecodingError.keyNotFound(_, let context):
                self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
              case DecodingError.dataCorrupted(let key):
                self?.errorMessage = "\(error.localizedDescription): \(key)"
              default:
                self?.errorMessage = "Error decoding document: \(error.localizedDescription)"
              }
              return nil
            }
          }
        }
    }
  }
  
  func addColorEntry() {
    let collectionRef = db.collection("colors")
    do {
      let newDocReference = try collectionRef.addDocument(from: newColor)
      print("ColorEntry stored with new document reference: \(newDocReference)")
    }
    catch {
      print(error)
    }
  }
}

Bu yayında kullanılan tüm kod snippet'leri, bu GitHub deposundan indirebileceğiniz bir örnek uygulamanın parçasıdır.

Hemen Codable'ı kullanmaya başlayın.

Swift'in Codable API'si, seri hale getirilmiş biçimlerdeki verileri uygulama veri modelinize ve uygulama veri modelinizden eşlemek için güçlü ve esnek bir yöntem sunar. Bu kılavuzda, Cloud Firestore'yı veri deposu olarak kullanan uygulamalarda kullanmanın ne kadar kolay olduğunu gördünüz.

Basit veri türlerinin bulunduğu temel bir örnekten başlayarak veri modelinin karmaşıklığını kademeli olarak artırdık. Bu sırada, eşlemeyi bizim için gerçekleştirmek üzere Codable ve Firebase'in uygulamasından yararlanabildik.

Codable hakkında daha fazla bilgi için aşağıdaki kaynakları incelemenizi öneririz:

Cloud Firestore belgelerini eşleme konusunda kapsamlı bir rehber oluşturmak için elimizden geleni yapsak da bu rehber eksiksiz değildir ve türlerinizi eşlemek için başka stratejiler kullanıyor olabilirsiniz. Aşağıdaki Geri bildirim gönder düğmesini kullanarak, diğer Cloud Firestore veri türlerini eşlemek veya verileri Swift'te temsil etmek için kullandığınız stratejileri bize bildirin.

Cloud Firestore'nın Codable desteğini kullanmamak için hiçbir neden yoktur.