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]?
पर मैप करने वाली डिक्शनरी दिखाता है.
इसका मतलब है कि हम हर फ़ील्ड को ऐक्सेस करने के लिए, Swift के सबस्क्रिप्ट सिंटैक्स का इस्तेमाल कर सकते हैं.
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, कोड में बदले जा सकने वाले और डिकोड किए जा सकने वाले प्रोटोकॉल के लिए, टाइप का दूसरा नाम है. इस प्रोटोकॉल के मुताबिक 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 टाइप पर मैप करने के लिए, यह तरीका अपनाएं:
- पक्का करें कि आपने अपने प्रोजेक्ट में
FirebaseFirestore
फ़्रेमवर्क जोड़ा हो. ऐसा करने के लिए, Swift Package Manager या CocoaPods में से किसी एक का इस्तेमाल किया जा सकता है. FirebaseFirestore
को अपनी Swift फ़ाइल में इंपोर्ट करें.- अपने टाइप को
Codable
के मुताबिक बनाएं. - (ज़रूरी नहीं, अगर आपको टाइप को
List
व्यू में इस्तेमाल करना है) अपने टाइप मेंid
प्रॉपर्टी जोड़ें. साथ ही,@DocumentID
का इस्तेमाल करके Cloud Firestore को बताएं कि इसे दस्तावेज़ आईडी से मैप करें. हम इस बारे में ज़्यादा जानकारी यहां देंगे. - किसी दस्तावेज़ के रेफ़रंस को Swift टाइप पर मैप करने के लिए,
documentReference.data(as: )
का इस्तेमाल करें. - Swift टाइप से डेटा को Cloud Firestore दस्तावेज़ में मैप करने के लिए,
documentReference.setData(from: )
का इस्तेमाल करें. - (ज़रूरी नहीं, लेकिन इसका सुझाव दिया जाता है) गड़बड़ी को ठीक करने का सही तरीका लागू करें.
इसके हिसाब से, 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 में ऐसा करने का सबसे आसान तरीका है, मैप का इस्तेमाल करना:
इससे जुड़ा 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 से पढ़ने या उसमें लिखने के लिए, कोड में बदलाव करने की ज़रूरत नहीं है: यह सब उस कोड के साथ काम करता है जिसे हमने शुरुआती सेक्शन में लिखा है.
कलेक्शन
कभी-कभी, हमें किसी दस्तावेज़ में वैल्यू का कलेक्शन सेव करना होता है. किसी किताब की शैलियां एक अच्छा उदाहरण हैं: द हिचहाइकर गाइड टू द गैलेक्सी जैसी किताब कई कैटगरी में आ सकती है — इस मामले में "साइंस-फ़िक्शन" और "कॉमेडी":
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]
}
यह कोड में बदले जा सकने वाले किसी भी टाइप के लिए काम करता है. इसलिए, हम कस्टम टाइप का भी इस्तेमाल कर सकते हैं. मान लें कि हमें हर किताब के लिए टैग की सूची सेव करनी है. टैग के नाम के साथ-साथ, हम टैग का रंग भी सेव करना चाहते हैं. जैसे:
टैग को इस तरह से सेव करने के लिए, हमें टैग को दिखाने और उसे कोड में बदलने के लिए, 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]
}
दस्तावेज़ आईडी को मैप करने के बारे में खास जानकारी
ज़्यादा टाइप को मैप करने से पहले, दस्तावेज़ आईडी को मैप करने के बारे में थोड़ी देर के लिए बात करते हैं.
हमने अपने कुछ पिछले उदाहरणों में @DocumentID
प्रॉपर्टी रैपर का इस्तेमाल किया है. ऐसा इसलिए किया गया, ताकि हम अपने Cloud Firestore दस्तावेज़ों के दस्तावेज़ आईडी को, Swift टाइप की id
प्रॉपर्टी से मैप कर सकें. ऐसा कई वजहों से ज़रूरी है:
- इससे हमें यह जानने में मदद मिलती है कि उपयोगकर्ता के स्थानीय बदलाव करने पर, किस दस्तावेज़ को अपडेट करना है.
- SwiftUI के
List
के लिए ज़रूरी है कि उसके एलिमेंटIdentifiable
हों, ताकि एलिमेंट डालने पर वे एक जगह से दूसरी जगह न जाएं.
ध्यान दें कि दस्तावेज़ को वापस लिखते समय, @DocumentID
के तौर पर मार्क किए गए एट्रिब्यूट को Cloud Firestore के एन्कोडर से एन्कोड नहीं किया जाएगा. ऐसा इसलिए है, क्योंकि दस्तावेज़ आईडी, दस्तावेज़ का एट्रिब्यूट नहीं है. इसलिए, इसे दस्तावेज़ में लिखना गलत होगा.
नेस्ट किए गए टाइप (जैसे, इस गाइड के पिछले उदाहरण में Book
पर टैग का कलेक्शन) के साथ काम करते समय, @DocumentID
प्रॉपर्टी जोड़ना ज़रूरी नहीं है: नेस्ट की गई प्रॉपर्टी, Cloud Firestore दस्तावेज़ का हिस्सा होती हैं और ये अलग दस्तावेज़ नहीं होतीं. इसलिए, उन्हें दस्तावेज़ आईडी की ज़रूरत नहीं होती.
तारीख और समय
Cloud Firestore में तारीखों और समय को मैनेज करने के लिए, पहले से मौजूद डेटा टाइप होता है. साथ ही, Cloud Firestore में Codable के लिए सहायता उपलब्ध होने की वजह से, इनका इस्तेमाल करना आसान हो जाता है.
आइए इस दस्तावेज़ पर एक नज़र डालें. इसमें प्रोग्रामिंग की सभी भाषाओं की जननी, Ada के बारे में बताया गया है. इसे 1843 में बनाया गया था:
इस दस्तावेज़ को मैप करने के लिए, 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)
जगह के हिसाब से दस्तावेज़ों को खोजने के बारे में ज़्यादा जानने के लिए, इस समाधान गाइड को देखें.
एनम्स
शायद 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
का इस्तेमाल कर सकते हैं. यह एक ऐसा एन्यम है जिसे कोड किए जा सकने वाले स्ट्रक्चर में जोड़ा जा सकता है. इससे यह तय किया जा सकता है कि कुछ एट्रिब्यूट को कैसे मैप किया जाएगा.
इस दस्तावेज़ को देखें:
इस दस्तावेज़ को ऐसे स्ट्रक्चर से मैप करने के लिए जिसकी नाम प्रॉपर्टी 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 के बारे में ज़्यादा जानने के लिए, ये संसाधन देखें:
- जॉन सुंडेल ने Codable के बुनियादी सिद्धांतों के बारे में एक अच्छा लेख लिखा है.
- अगर आपको किताबें पढ़ने में दिलचस्पी है, तो Mattt की Swift Codable के लिए फ़्लाइट स्कूल गाइड देखें.
- आखिर में, Donny Wals की Codable के बारे में पूरी सीरीज़ है.
हमने Cloud Firestore दस्तावेज़ों को मैप करने के लिए, पूरी जानकारी वाली गाइड बनाने की पूरी कोशिश की है. हालांकि, इसमें सभी जानकारी नहीं दी गई है. हो सकता है कि आप अपने दस्तावेज़ों को मैप करने के लिए, दूसरी रणनीतियों का इस्तेमाल कर रहे हों. नीचे दिए गए सुझाव/राय दें या शिकायत करें बटन का इस्तेमाल करके, हमें बताएं कि आपने Cloud Firestore डेटा के दूसरे टाइप को मैप करने या Swift में डेटा को दिखाने के लिए कौनसी रणनीतियां अपनाई हैं.
Cloud Firestore की Codable सहायता का इस्तेमाल न करने की कोई वजह नहीं है.