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