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

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

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

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

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

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

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

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

ودعنا لا ننسى أننا لا نستفيد من نوع سويفت القو�� ��ل���� ��عر�� ��ال��بط النوع الصحيح لكل خاصية من خصائص Book

ما هو Codable، على أي حال؟

ووفقًا لوثائق Apple، فإن Codable هو "نوع يمكنه تحويل نفسه داخل وخارج تمثيل خارجي". في الواقع، Codable هو نوع بديل لبروتوكولات Encodable والبروتوكولات الارتكازية. من خلال مطابقة نوع 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 أو 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، من السهل واستخدامها.

لنلقِ نظرة على هذا المستند الذي يعرض الأم للغات البرمجة "آدا" التي ظهرت في عام 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. على سبيل المثال، قد يكون أحد زملاء العمل مطوّر برامج بايثون، وقرر اختر 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 التي قد نرغب في استخدامه كمعرّف في طريقة عرض SwiftUI List. إذا لم نحدده في 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 تعتمد سويفت مفهوم الاختيارات للإشارة إلى بدون قيمة، ويتيح Cloud Firestore أيضًا قيم null. ومع ذلك، فإن السلوك التلقائي لخيارات الترميز التي لها قيمة nil هو لحذفها فقط. تمنحنا @ExplicitNull بعض التحكم في كيفية اختيارية عند تشفيرها: من خلال وضع علامة على إحدى الخصائص الاختيارية على أنها @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)
    }
  }
}

تُعد جميع مقتطفات الرمز المستخدمة في هذه المشاركة جزءًا من نموذج تطبيق يمكنك تنزيلها من مستودع جيت هب هذا.

تقدم واستخدم Codable!

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

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

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

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

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