تتيح لنا واجهة Codable API في 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]?.
هذا يعني أنه يمكننا استخدام صيغة subscript في 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، اتّبِع الخطوات التالية:
- تأكَّد من إضافة إطار عمل
FirebaseFirestoreإلى مشروعك. يمكنك استخدام Swift Package Manager أو CocoaPods لإجراء ذلك. - استورِد
FirebaseFirestoreإلى ملف Swift. - يجب أن يتوافق نوع البيانات مع
Codable. - (اختياري، إذا أردت استخدام النوع في طريقة عرض
List) أضِف سمةidإلى النوع، واستخدِم@DocumentIDلإعلام Cloud Firestore بضرورة ربط ذلك بمعرّف المستند. سنناقش ذلك بمزيد من التفصيل أدناه. - استخدِم
documentReference.data(as: )لربط مرجع مستند بنوع Swift. - استخدِم
documentReference.setData(from: )لربط البيانات من أنواع Swift بمستند Cloud Firestore. - (اختياري، ولكن يُنصح به بشدة) اتّبِع إجراءات فعالة لمعالجة الأخطاء.
لنعدّل نوع 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 هي استخدام خريطة:

عند كتابة بنية 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: كل ذلك يعمل مع الرمز الذي كتبناه في القسم الأولي.
الصفائف
في بعض الأحيان، نريد تخزين مجموعة من القيم في مستند. تُعدّ أنواع الكتب مثالاً جيدًا على ذلك: قد يندرج كتاب مثل The Hitchhiker's Guide to the Galaxy ضمن فئات متعددة، وفي هذه الحالة "خيال علمي" و "كوميديا":

في Cloud Firestore، يمكننا وضع نموذج لذلك باستخدام مصفوفة من القيم. هذا متاح لأي نوع قابل للترميز (مثل String وInt وما إلى ذلك). يوضّح المثال التالي كيفية إضافة مصفوفة من الأنواع إلى نموذج Book:
public struct BookWithGenre: Codable {
@DocumentID var id: String?
var title: String
var numberOfPages: Int
var author: String
var genres: [String]
}
بما أنّ هذه الطريقة تعمل مع أي نوع قابل للترميز، يمكننا استخدام أنواع مخصّصة أيضًا. لنفترض أنّنا نريد تخزين قائمة بالعلامات لكل كتاب. إلى جانب اسم العلامة، نريد تخزين لون العلامة أيضًا، على النحو التالي:

لتخزين العلامات بهذه الطريقة، كل ما علينا فعله هو تنفيذ بنية Tag لتمثيل علامة وجعلها قابلة للترميز:
struct Tag: Codable, Hashable {
var title: String
var color: String
}
بهذه الطريقة، يمكننا تخزين مصفوفة من 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:

قد يبدو نوع Swift لربط هذا المستند على النحو التالي:
struct ProgrammingLanguage: Codable {
@DocumentID var id: String?
var name: String
var year: Date
}
لا يمكننا ترك هذا القسم حول التواريخ والأوقات بدون إجراء محادثة حول @ServerTimestamp. يُعدّ برنامج تضمين المواقع هذا أداة فعّالة عندما يتعلق الأمر
بالتعامل مع الطوابع الزمنية في تطبيقك.
في أي نظام موزَّع، من المحتمل ألا تكون الساعات على الأنظمة الفردية متزامنة تمامًا طوال الوقت. قد تعتقد أنّ هذا ليس أمرًا مهمًا، ولكن تخيّل الآثار المترتبة على تشغيل ساعة غير متزامنة قليلاً لنظام تداول الأسهم: حتى الانحراف بمقدار ميلي ثانية قد يؤدي إلى اختلاف بملايين الدولارات عند تنفيذ عملية تداول.
Cloud Firestore handles attributes marked with @ServerTimestamp as
follows: if the attribute is nil when you store it (using addDocument(), for
example), Cloud Firestore will populate the field with the current server
timestamp at the time of writing it into the database. إذا لم يكن الحقل 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. هذا تعداد يمكننا إضافته إلى بنية قابلة للترميز لتحديد كيفية تعيين سمات معينة.
لنفترض أنّ لدينا المستند التالي:

لربط هذا المستند ببنية تحتوي على سمة اسم من النوع 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 لكتابة هذه السمة في المستند بقيمة فارغة إذا كانت تحتوي على قيمة 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!
توفّر واجهة برمجة التطبيقات Codable API من Swift طريقة فعّالة ومرنة لربط البيانات من التنسيقات المتسلسلة من وإلى نموذج بيانات تطبيقاتك. في هذا الدليل، رأيت مدى سهولة استخدامها في التطبيقات التي تستخدم Cloud Firestore كمخزن بيانات.
بدءًا من مثال أساسي يتضمّن أنواع بيانات بسيطة، زدنا تدريجيًا من تعقيد نموذج البيانات، مع الاعتماد على Codable وتنفيذ Firebase لإجراء عملية الربط نيابةً عنا.
لمزيد من التفاصيل حول Codable، أنصحك بالاطّلاع على المراجع التالية:
- كتب "جون سونديل" مقالة رائعة حول أساسيات Codable.
- إذا كنت تفضّل الكتب، يمكنك الاطّلاع على دليل Flight School حول Swift Codable من تأليف "مات".
- أخيرًا، يقدّم "دوني والس" سلسلة كاملة حول Codable.
على الرغم من أنّنا بذلنا قصارى جهدنا لتجميع دليل شامل لربط مستندات Cloud Firestore، إلا أنّ هذا الدليل ليس شاملاً، وقد تستخدم استراتيجيات أخرى لربط أنواعك. باستخدام زر إرسال الملاحظات أدناه، أخبِرنا بالاستراتيجيات التي تستخدمها لربط أنواع أخرى من بيانات Cloud Firestore أو تمثيل البيانات في Swift.
ما مِن سبب يمنعك من استخدام ميزة Codable في Cloud Firestore.