ربط بيانات Cloud Firestore باستخدام Swift Codable

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

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

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

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

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

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

مع دعم Cloud Firestore لواجهة برمجة التطبيقات Codable في Swift، يصبح هذا الأمر أسهل بكثير:

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

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 يتضمّن اسم سمة بتنسيق snake_case

لربط هذا المستند ببنية تحتوي على سمة اسم من النوع 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 أسماء سمات أنواع 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 بكتابة هذه السمة في المستند بقيمة null إذا كانت تحتوي على قيمة nil.

استخدام برنامج ترميز وفك ترميز مخصّصَين لربط الألوان

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

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

يبدو أنّه يجب أن يكون هناك حل أفضل وأبسط. لماذا لا نستخدم ألوان الويب (أو، على وجه التحديد، تدوين ألوان CSS السداسية العشرية)؟ من السهل استخدامها (هي في الأساس مجرد سلسلة)، وهي تتوافق حتى مع الشفافية.

لكي نتمكّن من ربط Color في Swift بقيمتها السداسية العشرية، علينا إنشاء إضافة 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!

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

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

لمزيد من التفاصيل حول Codable، ننصحك بالاطّلاع على المراجع التالية:

  • كتب John Sundell مقالة رائعة حول الـ أساسيات Codable.
  • إذا كنت تفضّل الكتب، اطّلِع على Flight School Guide to Swift Codable من Mattt's .
  • أخيرًا، كتب Donny Wals سلسلة كاملة حول Codable .

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

ليس هناك أي سبب لعدم استخدام دعم Codable في Cloud Firestore.