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

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

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

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

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

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

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

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

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

تعيين البيانات

تخزِّن Cloud Firestore البيانات في مستندات تربط المفاتيح بالقيم. لجلب data من مستند فردي، يمكننا استدعاء 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)

الربط بالأنواع البسيطة والعكس في مستندات 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 نوع يحتوي على المستند المرتبط، أو خطأ في حال تعذّر 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، يمكنك بسهولة استخدامها.

لنلقِ نظرة على هذا المستند الذي يمثل الأم لكل لغات البرمجة، وهي "آدا" التي تم إنشاؤها في عام 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_cased

لربط هذا المستند ببنية لها خاصية اسم من النوع 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 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، ويحدّد كلّها تقريبًا ألوانًا كقائمة مصنّفة تتألف من مكوّنات 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، ننصحك بالاطّلاع على المراجع التالية:

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

لا يوجد سبب لعدم استخدام فريق الدعم في Cloud Firestore.