Swift Codable की मदद से Cloud Firestore डेटा को मैप करें

Swift 4 में लॉन्च किया गया Swift का Codable API, हमें कंपाइलर की सुविधा का फ़ायदा उठाने में मदद करता है. इससे, सीरियलाइज़ किए गए फ़ॉर्मैट से Swift टाइप में डेटा को आसानी से मैप किया जा सकता है.

हो सकता है कि आपने Codable का इस्तेमाल, वेब एपीआई से अपने ऐप्लिकेशन के डेटा मॉडल में डेटा मैप करने के लिए किया हो. इसके अलावा, डेटा मॉडल से वेब एपीआई में डेटा मैप करने के लिए भी इसका इस्तेमाल किया जा सकता है. हालांकि, यह इससे ज़्यादा सुविधाजनक है.

इस गाइड में, हम यह देखेंगे कि Codable का इस्तेमाल करके, डेटा को Cloud Firestore से Swift टाइप में और Swift टाइप से Cloud Firestore में कैसे मैप किया जा सकता है.

Cloud Firestore से कोई दस्तावेज़ फ़ेच करने पर, आपके ऐप्लिकेशन को कुंजी/वैल्यू के पेयर की एक डिक्शनरी मिलेगी. अगर एक से ज़्यादा दस्तावेज़ दिखाने वाले किसी ऑपरेशन का इस्तेमाल किया जाता है, तो आपको डिक्शनरी का कलेक्शन मिलेगा.

अब, Swift में सीधे डिक्शनरी का इस्तेमाल किया जा सकता है. साथ ही, ये आपको कुछ ऐसी सुविधाएं भी देती हैं जो शायद आपके काम के हों. हालांकि, यह तरीका सुरक्षित नहीं होता है और एट्रिब्यूट के नाम की गलत स्पेलिंग लिखकर, मुश्किल से मुश्किल गड़बड़ी की जानकारी देना आसान होता है. इसके अलावा, पिछले हफ़्ते उस नई सुविधा को भेजते समय जोड़े गए नए एट्रिब्यूट को मैप करना भूल जाते हैं.

पहले, कई डेवलपर ने इन कमियों को ठीक करने के लिए, एक आसान मैपिंग लेयर लागू की थी. इससे उन्हें डिक्शनरी को Swift टाइप में मैप करने में मदद मिली. हालांकि, इनमें से ज़्यादातर लागू करने के तरीके, Cloud Firestore दस्तावेज़ों और आपके ऐप्लिकेशन के डेटा मॉडल के मिलते-जुलते टाइप के बीच मैपिंग को मैन्युअल तरीके से तय करने पर आधारित होते हैं.

Swift के Codable API के लिए Cloud Firestore की सहायता से, यह काम करना बहुत आसान हो जाता है:

  • अब आपको मैन्युअल तरीके से कोई मैपिंग कोड लागू करने की ज़रूरत नहीं होगी.
  • अलग-अलग नामों वाले एट्रिब्यूट को मैप करने का तरीका आसानी से तय किया जा सकता है.
  • इसमें Swift के कई टाइप के लिए, पहले से सहायता मौजूद होती है.
  • साथ ही, कस्टम टाइप को मैप करने के लिए, सपोर्ट जोड़ना आसान है.
  • सबसे अच्छी बात: आसान डेटा मॉडल के लिए, आपको कोई मैपिंग कोड लिखने की ज़रूरत नहीं होगी.

डेटा मैप करना

Cloud Firestore, ऐसे दस्तावेज़ों में डेटा सेव करता है जो कुंजियों को वैल्यू से मैप करते हैं. किसी एक दस्तावेज़ से डेटा फ़ेच करने के लिए, DocumentSnapshot.data() को कॉल किया जा सकता है. यह फ़ंक्शन, फ़ील्ड के नामों को Any:func data() -> [String : Any]? पर मैप करने वाली डिक्शनरी दिखाता है.

इसका मतलब है कि हर फ़ील्ड को ऐक्सेस करने के लिए, हम स्विफ़्ट के सबस्क्रिप्ट सिंटैक्स का इस्तेमाल कर सकते हैं.

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

ऐसा लग सकता है कि इसे लागू करना आसान है, लेकिन यह कोड अस्थिर होता है, इसे मैनेज करना मुश्किल होता है, और इसमें गड़बड़ियां होने की संभावना ज़्यादा होती है.

जैसा कि आप देख सकते हैं, हम दस्तावेज़ के फ़ील्ड के डेटा टाइप के बारे में अनुमान लगा रहे हैं. ये सही हो सकते हैं या नहीं भी.

याद रखें कि कोई स्कीमा न होने की वजह से, संग्रह में आसानी से नया दस्तावेज़ जोड़ा जा सकता है और किसी फ़ील्ड के लिए कोई दूसरा टाइप चुना जा सकता है. हो सकता है कि आपने numberOfPages फ़ील्ड के लिए, गलती से स्ट्रिंग चुन ली हो. इससे, मैपिंग से जुड़ी ऐसी समस्या हो सकती है जिसे ढूंढना मुश्किल हो. साथ ही, जब भी कोई नया फ़ील्ड जोड़ा जाएगा, तो आपको अपना मैपिंग कोड अपडेट करना होगा, जो कि काफ़ी जटिल होता है.

साथ ही, यह भी ध्यान रखें कि हम Swift के स्ट्रॉन्ग टाइप सिस्टम का फ़ायदा नहीं ले रहे हैं. यह सिस्टम, Book की हर प्रॉपर्टी के लिए सही टाइप को अच्छी तरह से जानता है.

वैसे, Codable क्या है?

Apple के दस्तावेज़ के मुताबिक, Codable "एक ऐसा टाइप है जो खुद को बाहरी प्रतिनिधित्व में बदल सकता है और उससे बाहर निकल सकता है." असल में, Codable, Encodable और Decodable प्रोटोकॉल के लिए एक टाइप का दूसरा नाम है. इस प्रोटोकॉल के मुताबिक Swift टाइप को फ़ॉर्मैट करने पर, कंपाइलर उस टाइप के किसी इंस्टेंस को JSON जैसे सीरियलाइज़ किए गए फ़ॉर्मैट से कोड में बदलने/कोड से इंस्टेंस में बदलने के लिए ज़रूरी कोड सिंथेसाइज़ करेगा.

किसी किताब का डेटा सेव करने के लिए, डेटा का एक आसान टाइप कुछ ऐसा दिख सकता है:

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

यह देखा जा सकता है कि Codable के टाइप की पुष्टि करना बहुत कम मुश्किल है. हमें सिर्फ़ प्रोटोकॉल का पालन करना था; इसके अलावा, किसी और बदलाव की ज़रूरत नहीं थी.

इस सुविधा की मदद से, अब हम किसी किताब को JSON ऑब्जेक्ट में आसानी से कोड कर सकते हैं:

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)")
}

JSON ऑब्जेक्ट को Book इंस्टेंस में डिकोड करने का तरीका:

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

Codable का इस्तेमाल करके, Cloud Firestore दस्तावेज़ों
में सिंपल टाइप में मैप करना और उनसे मैप करना

Cloud Firestore में सामान्य स्ट्रिंग से लेकर नेस्ट किए गए मैप तक, तरह-तरह के डेटा टाइप काम करते हैं. इनमें से ज़्यादातर, सीधे तौर पर Swift के पहले से मौजूद टाइप से जुड़े होते हैं. इससे पहले कि हम ज़्यादा जटिल डेटा टाइप के बारे में बात करें, आइए कुछ आसान डेटा टाइप को मैप करने पर नज़र डालते हैं.

Cloud Firestore दस्तावेज़ों को Swift टाइप पर मैप करने के लिए, यह तरीका अपनाएं:

  1. पक्का करें कि आपने अपने प्रोजेक्ट में FirebaseFirestore फ़्रेमवर्क जोड़ा हो. ऐसा करने के लिए, स्विफ़्ट पैकेज मैनेजर या CocoaPods का इस्तेमाल किया जा सकता है.
  2. अपनी Swift फ़ाइल में FirebaseFirestore इंपोर्ट करें.
  3. टाइप को Codable के हिसाब से बनाएं.
  4. (ज़रूरी नहीं, अगर आपको टाइप को List व्यू में इस्तेमाल करना है) अपने टाइप में id प्रॉपर्टी जोड़ें. साथ ही, Cloud Firestore को दस्तावेज़ आईडी से मैप करने के लिए, @DocumentID का इस्तेमाल करें. हम इस बारे में ज़्यादा जानकारी यहां देंगे.
  5. किसी दस्तावेज़ के रेफ़रंस को Swift टाइप से मैप करने के लिए, documentReference.data(as: ) का इस्तेमाल करें.
  6. Swift टाइप के डेटा को Cloud Firestore दस्तावेज़ पर मैप करने के लिए, documentReference.setData(from: ) का इस्तेमाल करें.
  7. (ज़रूरी नहीं, लेकिन इसका सुझाव दिया जाता है) गड़बड़ी को ठीक करने का सही तरीका लागू करें.

इसके हिसाब से, Book टाइप को अपडेट करते हैं:

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

इस टाइप को पहले से ही कोड में बदला जा सकता था. इसलिए, हमें सिर्फ़ id प्रॉपर्टी जोड़नी थी और @DocumentID प्रॉपर्टी रैपर के साथ एनोटेट करना था.

किसी दस्तावेज़ को फ़ेच करने और मैप करने के लिए, पिछले कोड स्निपेट का इस्तेमाल करके, मैन्युअल मैपिंग के सभी कोड को एक लाइन से बदला जा सकता है:

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:) को कॉल करते समय, दस्तावेज़ का टाइप बताकर, इसे और भी कम शब्दों में लिखा जा सकता है. इससे आपके लिए मैपिंग की जाएगी और मैप किए गए दस्तावेज़ वाला Result टाइप दिखेगा या डिकोड न हो पाने पर गड़बड़ी दिखेगी:

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)"
    }
  }
}

किसी मौजूदा दस्तावेज़ को अपडेट करना उतना ही आसान है जितना कि documentReference.setData(from: ) को कॉल करना. गड़बड़ी को मैनेज करने के कुछ बुनियादी तरीकों के साथ, Book इंस्टेंस को सेव करने का कोड यहां दिया गया है:

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

नया दस्तावेज़ जोड़ने पर, Cloud Firestore दस्तावेज़ को अपने-आप नया दस्तावेज़ आईडी असाइन कर देगा. यह तब भी काम करता है, जब ऐप्लिकेशन फ़िलहाल ऑफ़लाइन हो.

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 कई अन्य डेटा टाइप के साथ काम करता है. इनमें से कुछ स्ट्रक्चर्ड टाइप हैं, जिनका इस्तेमाल किसी दस्तावेज़ में नेस्ट किए गए ऑब्जेक्ट बनाने के लिए किया जा सकता है.

नेस्ट किए गए कस्टम टाइप

ज़्यादातर एट्रिब्यूट जिन्हें हम अपने दस्तावेज़ों में मैप करना चाहते हैं, वे आसान वैल्यू होते हैं. जैसे, किताब का टाइटल या लेखक का नाम. लेकिन उन मामलों में क्या होगा जब हमें किसी ज़्यादा कॉम्प्लेक्स ऑब्जेक्ट को स्टोर करना हो? उदाहरण के लिए, हो सकता है कि हम किताब के कवर में यूआरएल को अलग-अलग रिज़ॉल्यूशन में सेव करना चाहें.

Cloud Firestore में ऐसा करने का सबसे आसान तरीका है, मैप का इस्तेमाल करना:

Firestore दस्तावेज़ में नेस्ट किया गया कस्टम टाइप स्टोर करना

इससे जुड़ा Swift स्ट्रक्चर लिखते समय, हम इस बात का फ़ायदा ले सकते हैं कि Cloud Firestore में यूआरएल काम करते हैं — यूआरएल वाले फ़ील्ड को सेव करते समय, उसे स्ट्रिंग में बदल दिया जाएगा और इसके उलट भी:

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 दस्तावेज़ में कवर मैप के लिए, CoverImages स्ट्रक्चर के बारे में कैसे बताया है. BookWithCoverImages पर कवर प्रॉपर्टी को ज़रूरी के बजाय वैकल्पिक के तौर पर मार्क करके, हम इस बात को मैनेज कर सकते हैं कि कुछ दस्तावेज़ों में कवर एट्रिब्यूट मौजूद न हो.

अगर आपको यह जानना है कि डेटा फ़ेच करने या अपडेट करने के लिए कोई कोड स्निपेट क्यों नहीं है, तो आपको यह जानकर खुशी होगी कि Cloud Firestore से पढ़ने या उसमें लिखने के लिए, कोड में बदलाव करने की ज़रूरत नहीं है: यह सब उस कोड के साथ काम करता है जिसे हमने शुरुआती सेक्शन में लिखा है.

ऐरे

कभी-कभी, हम किसी दस्तावेज़ में वैल्यू का कलेक्शन सेव करना चाहते हैं. किताब की शैलियां एक अच्छे उदाहरण हैं: द हिचहाइकर की गाइड टू द गैलेक्सी जैसी किताब, कई कैटगरी में आ सकती है — इस मामले में "साइंस-फ़ाई" और "कॉमेडी":

Firestore दस्तावेज़ में ऐरे सेव करना

Cloud Firestore में, हम वैल्यू के कलेक्शन का इस्तेमाल करके इसे मॉडल कर सकते हैं. यह किसी भी कोड किए जा सकने वाले टाइप (जैसे, String, Int वगैरह) के साथ काम करता है. यहां दिए गए उदाहरण में, Book मॉडल में कई शैलियों को जोड़ने का तरीका बताया गया है:

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

यह किसी भी कोडेबल टाइप के लिए काम करता है. इसलिए, हम कस्टम टाइप का भी इस्तेमाल कर सकते हैं. मान लें कि हमें हर किताब के लिए टैग की सूची सेव करनी है. हम टैग के नाम के साथ-साथ, टैग का रंग भी स्टोर करना चाहते हैं. जैसे:

Firestore दस्तावेज़ में कस्टम टाइप का कलेक्शन सेव करना

टैग को इस तरह से सेव करने के लिए, हमें टैग को दिखाने और उसे कोड में बदलने के लिए, Tag स्ट्रक्चर लागू करना होगा:

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

इस तरह, हम अपने Book दस्तावेज़ों में Tags के कलेक्शन को सेव कर सकते हैं!

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

दस्तावेज़ के आईडी को मैप करने के बारे में कुछ जानकारी

ज़्यादा टाइप को मैप करने से पहले, आइए दस्तावेज़ आईडी को मैप करने के बारे में थोड़ी देर के लिए बात करते हैं.

हमने अपने Cloud Firestore दस्तावेज़ों के दस्तावेज़ आईडी को, Swift टाइप की id प्रॉपर्टी पर मैप करने के लिए, पिछले कुछ उदाहरणों में @DocumentID प्रॉपर्टी रैपर का इस्तेमाल किया है. ऐसा कई वजहों से ज़रूरी है:

  • इससे हमें यह जानने में मदद मिलती है कि उपयोगकर्ता के स्थानीय बदलाव करने पर, किस दस्तावेज़ को अपडेट करना है.
  • SwiftUI के List के लिए ज़रूरी है कि उसके एलिमेंट Identifiable हों, ताकि एलिमेंट डालने पर वे एक जगह से दूसरी जगह न जाएं.

ध्यान दें कि दस्तावेज़ को वापस लिखते समय, @DocumentID के तौर पर मार्क किए गए एट्रिब्यूट को Cloud Firestore का एन्कोडर, कोड में बदल नहीं देगा. ऐसा इसलिए है, क्योंकि दस्तावेज़ आईडी, दस्तावेज़ का एट्रिब्यूट नहीं है. इसलिए, इसे दस्तावेज़ में लिखना गलत होगा.

नेस्ट किए गए टाइप (जैसे, इस गाइड के पिछले उदाहरण में Book पर टैग का कलेक्शन) के साथ काम करते समय, @DocumentID प्रॉपर्टी जोड़ना ज़रूरी नहीं है: नेस्ट की गई प्रॉपर्टी, Cloud Firestore दस्तावेज़ का हिस्सा होती हैं और ये अलग दस्तावेज़ नहीं होतीं. इसलिए, उन्हें दस्तावेज़ आईडी की ज़रूरत नहीं होती.

तारीख और समय

Cloud Firestore में तारीखों और समय को मैनेज करने के लिए, पहले से मौजूद डेटा टाइप होता है. साथ ही, Cloud Firestore में Codable के लिए सहायता उपलब्ध होने की वजह से, इनका इस्तेमाल करना आसान हो जाता है.

आइए, इस दस्तावेज़ पर नज़र डालते हैं, जो सभी प्रोग्रामिंग भाषाओं की जननी है, अडा का आविष्कार 1843 में हुआ था:

Firestore दस्तावेज़ में तारीखें सेव करना

इस दस्तावेज़ को मैप करने के लिए Swift टाइप ऐसा दिख सकता है:

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

हम @ServerTimestamp के बारे में बातचीत किए बिना तारीखों और समय के बारे में यह सेक्शन नहीं छोड़ सकते. आपके ऐप्लिकेशन में टाइमस्टैंप मैनेज करने के लिए, यह प्रॉपर्टी रैपर बहुत कारगर है.

किसी भी डिस्ट्रिब्यूटेड सिस्टम में, हो सकता है कि अलग-अलग सिस्टम के क्लॉक, हर समय पूरी तरह सिंक न हों. आपको शायद लगे कि यह कोई बड़ी बात नहीं है, लेकिन स्टॉक ट्रेड सिस्टम के लिए, थोड़ी देर से चलने वाली घड़ी के असर के बारे में सोचें: ट्रेड करते समय, एक मिलीसेकंड के अंतर से भी लाखों डॉलर का अंतर हो सकता है.

Cloud Firestore, @ServerTimestamp के साथ मार्क किए गए एट्रिब्यूट को इस तरह मैनेज करता है: अगर एट्रिब्यूट को स्टोर करते समय (उदाहरण के लिए, addDocument() का इस्तेमाल करके) उसकी वैल्यू nil है, तो Cloud Firestore डेटाबेस में लिखते समय, फ़ील्ड को मौजूदा सर्वर टाइमस्टैंप से पॉप्युलेट कर देगा. addDocument() या updateData() को कॉल करने पर, अगर फ़ील्ड nil नहीं है, तो Cloud Firestore एट्रिब्यूट की वैल्यू में कोई बदलाव नहीं करेगा. इस तरह, createdAt और lastUpdatedAt जैसे फ़ील्ड को लागू करना आसान है.

जियोपॉइंट

हमारे ऐप्लिकेशन में जगह की जानकारी का इस्तेमाल हर जगह किया जाता है. कई रोमांचक सुविधाएं उन्हें संग्रहित करने से संभव हो जाती हैं. उदाहरण के लिए, किसी टास्क के लिए जगह की जानकारी सेव करना मददगार हो सकता है. इससे किसी डेस्टिनेशन पर पहुंचने पर, आपका ऐप्लिकेशन आपको उस टास्क के बारे में याद दिला सकता है.

Cloud Firestore में एक डेटा टाइप GeoPoint पहले से मौजूद होता है, जिसमें किसी भी जगह का देशांतर और अक्षांश सेव किए जा सकते हैं. Cloud Firestore दस्तावेज़ में मौजूद जगहों को मैप करने के लिए, GeoPoint टाइप का इस्तेमाल किया जा सकता है:

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

Swift में इसका टाइप CLLocationCoordinate2D है. इन दोनों टाइप के बीच, इस ऑपरेशन की मदद से मैप किया जा सकता है:

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

जगह के हिसाब से दस्तावेज़ों को खोजने के बारे में ज़्यादा जानने के लिए, इस समाधान गाइड को देखें.

एनम्स

Enums शायद Swift की सबसे कम रेटिंग वाली भाषा की सुविधाओं में से एक है; उनमें जो उम्मीद के मुताबिक नहीं है उससे कहीं ज़्यादा सुविधाएं हैं. ईनम का एक सामान्य उपयोग उदाहरण किसी चीज़ की असंतत स्थितियों को मॉडल करना है. उदाहरण के लिए, हम शायद लेखों को मैनेज करने के लिए कोई ऐप्लिकेशन लिख रहे हैं. किसी लेख की स्थिति को ट्रैक करने के लिए, हो सकता है कि हम एनम Status का इस्तेमाल करना चाहें:

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

Cloud Firestore में, नेटिव तौर पर ईनम की सुविधा नहीं दी जाती.इसका मतलब है कि यह वैल्यू के सेट को लागू नहीं कर सकती. हालांकि, हम अब भी इस बात का इस्तेमाल कर सकते हैं कि ईनम को टाइप किया जा सकता है और कोडिंग का कोई टाइप चुना जा सकता है. इस उदाहरण में, हमने String चुना है. इसका मतलब है कि Cloud Firestore दस्तावेज़ में सेव किए जाने पर, सभी एनम वैल्यू को स्ट्रिंग से/स्ट्रिंग पर मैप किया जाएगा.

साथ ही, Swift में कस्टम रॉ वैल्यू का इस्तेमाल किया जा सकता है. इसलिए, हम यह भी तय कर सकते हैं कि कौनसी वैल्यू, किस एनम केस से जुड़ी है. उदाहरण के लिए, अगर हमने Status.inReview केस को "समीक्षा में है" के तौर पर सेव करने का फ़ैसला लिया है, तो हम ऊपर दी गई सूची को इस तरह अपडेट कर सकते हैं:

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

मैपिंग को पसंद के मुताबिक बनाना

कभी-कभी, Cloud Firestore दस्तावेज़ों के एट्रिब्यूट के नाम, Swift में हमारे डेटा मॉडल की प्रॉपर्टी के नाम से मेल नहीं खाते. उदाहरण के लिए, हो सकता है कि हमारे साथ काम करने वाला कोई व्यक्ति Python डेवलपर हो और उसने अपने सभी एट्रिब्यूट के नाम के लिए snake_case को चुना हो.

चिंता न करें: Codable ने हमें इसकी जानकारी दी है!

ऐसे मामलों में, हम CodingKeys का इस्तेमाल कर सकते हैं. यह एक ईनम है, जिसे हम कुछ एट्रिब्यूट को मैप करने का तरीका बताने के लिए, कोड किए जा सकने वाले स्ट्रक्चर में जोड़ सकते हैं.

इस दस्तावेज़ को देखें:

snake_cased एट्रिब्यूट के नाम वाला Firestore दस्तावेज़

इस दस्तावेज़ को ऐसे स्ट्रक्चर से मैप करने के लिए जिसकी name प्रॉपर्टी String टाइप की है, हमें ProgrammingLanguage स्ट्रक्चर में CodingKeys एनम जोड़ना होगा. साथ ही, दस्तावेज़ में एट्रिब्यूट का नाम बताना होगा:

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 हमारे Swift टाइप की प्रॉपर्टी के नामों का इस्तेमाल करेगा. इससे, उन Cloud Firestore दस्तावेज़ों के एट्रिब्यूट के नामों का पता चलेगा जिन्हें हमें मैप करना है. इसलिए, जब तक एट्रिब्यूट के नाम मेल खाते हैं, तब तक कोड में बदले जा सकने वाले टाइप में CodingKeys को जोड़ने की ज़रूरत नहीं है. हालांकि, किसी खास टाइप के लिए CodingKeys का इस्तेमाल करने के बाद, हमें उन सभी प्रॉपर्टी के नाम जोड़ने होंगे जिन्हें हमें मैप करना है.

ऊपर दिए गए कोड स्निपेट में, हमने एक id प्रॉपर्टी दी है जिसे हम SwiftUI List व्यू में आइडेंटिफ़ायर के तौर पर इस्तेमाल कर सकते हैं. अगर हमने CodingKeys में इसकी जानकारी नहीं दी है, तो डेटा फ़ेच करते समय इसे मैप नहीं किया जाएगा और यह nil बन जाएगा. इससे List व्यू में पहला दस्तावेज़ दिखेगा.

अगर कोई प्रॉपर्टी, संबंधित CodingKeys enum के लिए केस के तौर पर सूची में नहीं है, तो मैपिंग की प्रोसेस के दौरान उसे अनदेखा कर दिया जाएगा. अगर हम खास तौर पर कुछ प्रॉपर्टी को मैप किए जाने से रोकना चाहते हैं, तो यह काफ़ी आसान हो सकता है.

उदाहरण के लिए, अगर हमें reasonWhyILoveThis प्रॉपर्टी को मैप किए जाने से बाहर रखना है, तो हमें बस उसे CodingKeys एनम से हटाना होगा:

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

कभी-कभी, हो सकता है कि हम Cloud Firestore दस्तावेज़ में खाली एट्रिब्यूट को फिर से लिखना चाहें. Swift में वैल्यू न होने की जानकारी देने के लिए, वैकल्पिक वैल्यू का इस्तेमाल किया जाता है. साथ ही, Cloud Firestore में null वैल्यू भी काम करती हैं. हालांकि, nil वैल्यू वाले वैकल्पिक एट्रिब्यूट को कोड में बदलने के लिए, डिफ़ॉल्ट तौर पर उन्हें हटा दिया जाता है. @ExplicitNull की मदद से, Swift के वैकल्पिक एलिमेंट को कोड में बदलने के तरीके को कंट्रोल किया जा सकता है: किसी वैकल्पिक प्रॉपर्टी को @ExplicitNull के तौर पर फ़्लैग करके, Cloud Firestore को यह निर्देश दिया जा सकता है कि अगर उसमें nil की वैल्यू है, तो इस प्रॉपर्टी को दस्तावेज़ में शून्य वैल्यू के साथ लिखें.

रंगों को मैप करने के लिए कस्टम एन्कोडर और डिकोडर का इस्तेमाल करना

Codable की मदद से डेटा को मैप करने के बारे में बताने वाले लेख के आखिरी विषय के तौर पर, आइए कस्टम एन्कोडर और डिकोडर के बारे में जानें. इस सेक्शन में कोई नेटिव Cloud Firestore डेटाटाइप शामिल नहीं है. हालांकि, Cloud Firestore के ऐप्लिकेशन में कस्टम एन्कोडर और डिकोडर काफ़ी मददगार हैं.

"मैं रंगों को कैसे मैप करूं", डेवलपर के सबसे ज़्यादा पूछे जाने वाले सवालों में से एक है. यह सवाल, Cloud Firestore के लिए ही नहीं, बल्कि Swift और JSON के बीच मैपिंग के लिए भी पूछा जाता है. इस समस्या को हल करने के कई तरीके हैं, लेकिन ज़्यादातर तरीके JSON पर आधारित होते हैं. साथ ही, ज़्यादातर तरीके रंगों को नेस्ट की गई डिक्शनरी के तौर पर मैप करते हैं. यह डिक्शनरी, रंगों के आरजीबी कॉम्पोनेंट से बनी होती है.

ऐसा लगता है कि इस समस्या को हल करने का कोई बेहतर और आसान तरीका होना चाहिए. हम वेब कलर (या ज़्यादा सटीक तरीके से कहें, तो सीएसएस हेक्स कलर नोटेशन) का इस्तेमाल क्यों नहीं करते — इनका इस्तेमाल करना आसान है (असल में, सिर्फ़ एक स्ट्रिंग) और ये पारदर्शी भी होते हैं!

Swift Color को उसकी हेक्स वैल्यू से मैप करने के लिए, हमें एक ऐसा Swift एक्सटेंशन बनाना होगा जो Color में Codable जोड़ता है.

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() का इस्तेमाल करके, हम String को उसके Color के बराबर डिकोड कर सकते हैं. इसके लिए, RGBA कॉम्पोनेंट को नेस्ट करने की ज़रूरत नहीं होती. इसके अलावा, आपके पास इन वैल्यू को कन्वर्ट किए बिना, अपने ऐप्लिकेशन के वेब यूज़र इंटरफ़ेस (यूआई) में इस्तेमाल करने का विकल्प है!

इसकी मदद से, हम टैग को मैप करने के लिए कोड अपडेट कर सकते हैं. इससे, अपने ऐप्लिकेशन के यूज़र इंटरफ़ेस (यूआई) कोड में मैन्युअल तरीके से टैग को मैप करने के बजाय, टैग के रंगों को सीधे मैनेज करना आसान हो जाता है:

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

गड़बड़ियों को मैनेज करना

ऊपर दिए गए कोड स्निपेट में, हमने गड़बड़ी को मैनेज करने की प्रोसेस को कम से कम रखा है. हालांकि, प्रोडक्शन ऐप्लिकेशन में, आपको यह पक्का करना होगा कि किसी भी गड़बड़ी को आसानी से मैनेज किया जा सके.

यहां एक कोड स्निपेट दिया गया है, जिसमें किसी भी गड़बड़ी को मैनेज करने का तरीका बताया गया है:

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)"
        }
      }
    }
  }
}

लाइव अपडेट में गड़बड़ियों को मैनेज करना

पिछले कोड स्निपेट में, किसी एक दस्तावेज़ को फ़ेच करते समय गड़बड़ियों को मैनेज करने का तरीका बताया गया है. डेटा को एक बार फ़ेच करने के अलावा, Cloud Firestore आपके ऐप्लिकेशन में अपडेट को तुरंत डिलीवर करने की सुविधा भी देता है. इसके लिए, स्नैपशॉट लिसनर का इस्तेमाल किया जाता है: हम किसी कलेक्शन (या क्वेरी) पर स्नैपशॉट लिसनर रजिस्टर कर सकते हैं. इसके बाद, जब भी कोई अपडेट होगा, Cloud Firestore हमारे लिसनर को कॉल करेगा.

यहां एक कोड स्निपेट दिया गया है. इसमें स्नैपशॉट लिसनर को रजिस्टर करने, Codable का इस्तेमाल करके डेटा को मैप करने, और किसी भी गड़बड़ी को मैनेज करने का तरीका बताया गया है. इसमें कलेक्शन में नया दस्तावेज़ जोड़ने का तरीका भी बताया गया है. जैसा कि आपको दिखेगा, मैप किए गए दस्तावेज़ों को होल्ड करके रखने वाले लोकल अरे को अपडेट करने की कोई ज़रूरत नहीं है, क्योंकि स्नैपशॉट लिसनर में कोड की मदद से यह काम किया जाता है.

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

इस पोस्ट में इस्तेमाल किए गए सभी कोड स्निपेट, सैंपल ऐप्लिकेशन का हिस्सा हैं. इसे इस GitHub रिपॉज़िटरी से डाउनलोड किया जा सकता है.

Codable का इस्तेमाल करें!

Swift का Codable API, डेटा को सीरियलाइज़ किए गए फ़ॉर्मैट से आपके ऐप्लिकेशन के डेटा मॉडल में और उससे मैप करने का बेहतर और आसान तरीका उपलब्ध कराता है. इस गाइड में, आपने देखा कि Cloud Firestore को डेटास्टोर के तौर पर इस्तेमाल करने वाले ऐप्लिकेशन में, इसे इस्तेमाल करना कितना आसान है.

सामान्य डेटा टाइप के साथ बुनियादी उदाहरण से शुरुआत करते हुए, हमने डेटा मॉडल की जटिलता को धीरे-धीरे बढ़ा दिया है. इस दौरान, हम अपने लिए मैपिंग करने के लिए, Codable और Firebase को लागू करने की प्रोसेस पर भरोसा कर पाए.

हमारा सुझाव है कि Codable के बारे में ज़्यादा जानने के लिए, ये संसाधन देखें:

हमने Cloud Firestore दस्तावेज़ों को मैप करने के लिए, पूरी जानकारी वाली गाइड बनाने की पूरी कोशिश की है. हालांकि, इसमें सभी जानकारी नहीं दी गई है. हो सकता है कि आप अपने दस्तावेज़ों को मैप करने के लिए, दूसरी रणनीतियों का इस्तेमाल कर रहे हों. नीचे दिए गए सुझाव/राय भेजें या शिकायत करें बटन का इस्तेमाल करके, हमें बताएं कि अन्य तरह के Cloud Firestore डेटा को मैप करने या Swift में डेटा को दिखाने के लिए, किन रणनीतियों का इस्तेमाल किया जा रहा है.

Cloud Firestore की Codable सहायता का इस्तेमाल न करने की कोई वजह नहीं है.