بيانات Map Cloud Firestore مع Swift Codable

تمكننا واجهة برمجة تطبيقات Swift's Codable، التي تم تقديمها في Swift 4، من الاستفادة من قوة المترجم لتسهيل تعيين البيانات من التنسيقات المتسلسلة إلى أنواع Swift.

ربما كنت تستخدم Codable لتعيين البيانات من واجهة برمجة تطبيقات الويب إلى نموذج بيانات تطبيقك (والعكس صحيح)، ولكنها أكثر مرونة من ذلك بكثير.

في هذا الدليل، سنلقي نظرة على كيفية استخدام Codable لتعيين البيانات من Cloud Firestore إلى أنواع Swift والعكس.

عند جلب مستند من Cloud Firestore، سيتلقى تطبيقك قاموسًا لأزواج المفاتيح/القيمة (أو مجموعة من القواميس، إذا كنت تستخدم إحدى العمليات لإرجاع مستندات متعددة).

الآن، يمكنك بالتأكيد الاستمرار في استخدام القواميس مباشرة في Swift، وهي توفر بعض المرونة الكبيرة التي قد تكون بالضبط ما تتطلبه حالة الاستخدام الخاصة بك. ومع ذلك، فإن هذا الأسلوب ليس آمنًا للكتابة ومن السهل تقديم أخطاء يصعب تعقبها عن طريق كتابة أسماء السمات بشكل خاطئ، أو نسيان تعيين السمة الجديدة التي أضافها فريقك عندما قاموا بشحن هذه الميزة الجديدة المثيرة الأسبوع الماضي.

في الماضي، عمل العديد من المطورين على التغلب على أوجه القصور هذه من خلال تنفيذ طبقة تعيين بسيطة سمحت لهم بتعيين القواميس لأنواع Swift. ولكن مرة أخرى، تعتمد معظم هذه التطبيقات على تحديد التعيين يدويًا بين مستندات Cloud Firestore والأنواع المقابلة لنموذج بيانات تطبيقك.

مع دعم Cloud Firestore لـ Swift's Codable API، يصبح هذا أسهل كثيرًا:

  • لن تضطر بعد الآن إلى تنفيذ أي تعليمات برمجية للتعيين يدويًا.
  • من السهل تحديد كيفية تعيين السمات بأسماء مختلفة.
  • يحتوي على دعم مدمج للعديد من أنواع 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 .

ما هو قابل للتشفير، على أي حال؟

وفقًا لوثائق شركة 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)

التعيين من وإلى الأنواع البسيطة في مستندات Cloud Firestore
باستخدام قابلة للتشفير

يدعم Cloud Firestore مجموعة واسعة من أنواع البيانات، بدءًا من السلاسل البسيطة وحتى الخرائط المتداخلة. تتوافق معظمها مباشرةً مع الأنواع المضمنة في Swift. دعونا نلقي نظرة على تعيين بعض أنواع البيانات البسيطة أولاً قبل أن نتعمق في الأنواع الأكثر تعقيدًا.

لتعيين مستندات Cloud Firestore لأنواع Swift، اتبع الخطوات التالية:

  1. تأكد من إضافة إطار عمل FirebaseFirestore إلى مشروعك. يمكنك استخدام Swift Package Manager أو CocoaPods للقيام بذلك.
  2. قم باستيراد FirebaseFirestore إلى ملف Swift الخاص بك.
  3. قم بمطابقة النوع الخاص بك إلى Codable .
  4. (اختياري، إذا كنت تريد استخدام النوع في طريقة عرض List ) أضف خاصية id إلى النوع الخاص بك، واستخدم @DocumentID لإخبار Cloud Firestore بتعيين ذلك لمعرف المستند. سنناقش هذا بمزيد من التفصيل أدناه.
  5. استخدم documentReference.data(as: ) لتعيين مرجع مستند إلى نوع Swift.
  6. استخدم documentReference.setData(from: ) لتعيين البيانات من أنواع Swift إلى مستند Cloud Firestore.
  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 عددًا من أنواع البيانات الأخرى، بعضها عبارة عن أنواع منظمة يمكنك استخدامها لإنشاء كائنات متداخلة داخل المستند.

أنواع مخصصة متداخلة

معظم السمات التي نريد تعيينها في مستنداتنا هي قيم بسيطة، مثل عنوان الكتاب أو اسم المؤلف. ولكن ماذا عن تلك الحالات التي نحتاج فيها إلى تخزين كائن أكثر تعقيدًا؟ على سبيل المثال، قد نرغب في تخزين عناوين URL لغلاف الكتاب بدرجات دقة مختلفة.

أسهل طريقة للقيام بذلك في Cloud Firestore هي استخدام الخريطة:

تخزين نوع مخصص متداخل في مستند Firestore

عند كتابة بنية Swift المقابلة، يمكننا الاستفادة من حقيقة أن Cloud Firestore يدعم عناوين URL - عند تخزين حقل يحتوي على عنوان URL، سيتم تحويله إلى سلسلة والعكس صحيح:

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

لاحظ كيف قمنا بتحديد بنية CoverImages لخريطة الغلاف في مستند Cloud Firestore. من خلال وضع علامة على خاصية الغلاف في 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
}

وبهذه الطريقة، يمكننا تخزين مجموعة من Tags في مستندات Book !

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

كلمة سريعة حول تعيين معرفات المستندات

قبل أن ننتقل إلى تعيين المزيد من الأنواع، دعنا نتحدث عن تعيين معرفات المستندات للحظة.

لقد استخدمنا مُجمِّع خاصية @DocumentID في بعض الأمثلة السابقة لتعيين معرف المستند الخاص بمستندات Cloud Firestore الخاصة بنا إلى خاصية id لأنواع Swift الخاصة بنا. وهذا أمر مهم لعدد من الأسباب:

  • يساعدنا ذلك في معرفة المستند الذي يجب تحديثه في حالة قيام المستخدم بإجراء تغييرات محلية.
  • تتطلب List SwiftUI أن تكون عناصرها Identifiable لمنع العناصر من التنقل عند إدراجها.

تجدر الإشارة إلى أن السمة التي تم وضع علامة عليها @DocumentID لن يتم تشفيرها بواسطة برنامج تشفير Cloud Firestore عند إعادة كتابة المستند. وذلك لأن معرف المستند ليس سمة للمستند نفسه - لذا فإن كتابته في المستند سيكون خطأ.

عند العمل مع الأنواع المتداخلة (مثل مجموعة العلامات الموجودة على Book في مثال سابق في هذا الدليل)، ليس من الضروري إضافة خاصية @DocumentID : الخصائص المتداخلة هي جزء من مستند Cloud Firestore، ولا تشكل وثيقة منفصلة. ومن ثم، فإنهم لا يحتاجون إلى معرف المستند.

التواريخ والأوقات

يحتوي Cloud Firestore على نوع بيانات مدمج للتعامل مع التواريخ والأوقات، وبفضل دعم Cloud Firestore لـ Codable، أصبح من السهل استخدامها.

دعونا نلقي نظرة على هذه الوثيقة التي تمثل أم جميع لغات البرمجة، وهي Ada، التي تم اختراعها عام 1843:

تخزين التواريخ في مستند Firestore

قد يبدو نوع Swift لتعيين هذا المستند كما يلي:

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

لا يمكننا ترك هذا القسم الخاص بالتواريخ والأوقات دون إجراء محادثة حول @ServerTimestamp . يعد غلاف الخاصية هذا بمثابة أداة فعالة عندما يتعلق الأمر بالتعامل مع الطوابع الزمنية في تطبيقك.

في أي نظام موزع، من المحتمل أن الساعات الموجودة على الأنظمة الفردية ليست متزامنة تمامًا طوال الوقت. قد تظن أن هذه ليست مشكلة كبيرة، ولكن تخيل الآثار المترتبة على عدم مزامنة الساعة قليلاً لنظام تداول الأسهم: فحتى الانحراف بالمللي ثانية قد يؤدي إلى اختلاف بملايين الدولارات عند تنفيذ التجارة.

يتعامل Cloud Firestore مع السمات المميزة بـ @ServerTimestamp على النحو التالي: إذا كانت السمة nil عند تخزينها (باستخدام addDocument() ، على سبيل المثال)، فسيقوم Cloud Firestore بملء الحقل بالطابع الزمني الحالي للخادم في وقت كتابته في قاعدة البيانات . إذا لم يكن الحقل nil عند استدعاء addDocument() أو updateData() ، فسيترك 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 . هذا تعداد يمكننا إضافته إلى بنية قابلة للتشفير لتحديد كيفية تعيين سمات معينة.

النظر في هذه الوثيقة:

مستند Firestore باسم سمة على شكل ثعبان

لتعيين هذا المستند إلى بنية تحتوي على خاصية اسم من النوع String ، نحتاج إلى إضافة تعداد CodingKeys إلى بنية ProgrammingLanguage ، وتحديد اسم السمة في المستند:

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 قد نرغب في استخدامها كمعرف في طريقة عرض List SwiftUI. إذا لم نحدده في CodingKeys ، فلن يتم تعيينه عند جلب البيانات، وبالتالي يصبح nil . قد يؤدي هذا إلى ملء عرض List بالمستند الأول.

سيتم تجاهل أي خاصية غير مدرجة كحالة في تعداد CodingKeys المعني أثناء عملية التعيين. يمكن أن يكون هذا مناسبًا في الواقع إذا أردنا على وجه التحديد استبعاد بعض الخصائص من التعيين.

لذلك، على سبيل المثال، إذا أردنا استبعاد الخاصية 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، وكلها تقريبًا تعين الألوان كقاموس متداخل يتكون من مكونات RGB الخاصة به.

يبدو أنه يجب أن يكون هناك حل أفضل وأبسط. لماذا لا نستخدم ألوان الويب (أو، لنكون أكثر تحديدًا، تدوين الألوان السداسية في CSS) - فهي سهلة الاستخدام (في الأساس مجرد سلسلة)، كما أنها تدعم الشفافية!

لكي نتمكن من تعيين Swift Color إلى قيمته السداسية، نحتاج إلى إنشاء ملحق Swift يضيف Codable إلى Color .

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's Codable طريقة قوية ومرنة لتعيين البيانات من التنسيقات المتسلسلة من وإلى نموذج بيانات تطبيقاتك. في هذا الدليل، رأيت مدى سهولة الاستخدام في التطبيقات التي تستخدم Cloud Firestore كمخزن بيانات خاص بها.

بدءًا من مثال أساسي بأنواع بيانات بسيطة، قمنا بزيادة تعقيد نموذج البيانات تدريجيًا، مع قدرتنا على الاعتماد على تطبيق Codable وFirebase لإجراء عملية التعيين نيابةً عنا.

لمزيد من التفاصيل حول Codable، أوصي بالموارد التالية:

على الرغم من أننا بذلنا قصارى جهدنا لتجميع دليل شامل لتعيين مستندات Cloud Firestore، إلا أن هذا ليس شاملاً، وربما تستخدم استراتيجيات أخرى لتعيين أنواعك. باستخدام زر إرسال التعليقات أدناه، أخبرنا بالإستراتيجيات التي تستخدمها لتعيين أنواع أخرى من بيانات Cloud Firestore أو تمثيل البيانات في Swift.

لا يوجد حقًا سبب لعدم استخدام دعم Cloud Firestore Codable.