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

في 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 يتعامل مع السمات التي تم وضع علامة @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. هذه عملية تعداد يمكننا إضافتها إلى بنية قابلة للترميز لتحديد كيفية ربط سمات معيّنة.
لنلقِ نظرة على هذا المستند:

لربط هذا المستند ببنية تحتوي على سمة اسم من النوع 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)
}
}
}
انطلقوا واستخدموا Codable!
توفّر واجهة برمجة التطبيقات Codable في Swift طريقة فعّالة ومرنة لربط البيانات من التنسيقات التسلسلية بنموذج بيانات تطبيقاتكم ومنه. في هذا الدليل، رأيتم مدى سهولة استخدامها في التطبيقات التي تستخدم Cloud Firestore كمخزن بيانات.
بدءًا من مثال أساسي باستخدام أنواع بيانات بسيطة، زدنا تدريجيًا من تعقيد نموذج البيانات، مع التمكّن في الوقت نفسه من الاعتماد على Codable وتنفيذ Firebase لإجراء عملية الربط نيابةً عنّا.
لمزيد من التفاصيل حول Codable، ننصحكم بالاطّلاع على المراجع التالية:
- لدى John Sundell مقالة رائعة حول الـ أساسيات Codable.
- إذا كنتم تفضلون الكتب، اطّلِعوا على Mattt's Flight School Guide to Swift Codable.
- أخيرًا، لدى Donny Wals سلسلة كاملة حول Codable .
مع أنّنا بذلنا قصارى جهدنا لتجميع دليل شامل لربط Cloud Firestore المستندات، فإنّ هذا الدليل ليس شاملاً، وقد تستخدمون استراتيجيات أخرى لربط أنواعكم. باستخدام الزر إرسال ملاحظات أدناه، يُرجى إعلامنا بالاستراتيجيات التي تستخدمونها لربط أنواع أخرى من Cloud Firestore البيانات أو تمثيل البيانات في Swift.
ما مِن سبب حقيقي لعدم استخدام دعم Codable في Cloud Firestore.