تتيح لنا واجهة برمجة التطبيقات Codable API في Swift، التي تم تقديمها في Swift 4، الاستفادة من فعالية compiler لتسهيل ربط البيانات من التنسيقات التسلسلية بأنواع Swift.
ربما كنت تستخدم Codable لربط البيانات من واجهة برمجة تطبيقات الويب بأحد نماذج data في تطبيقك (والعكس صحيح)، ولكنّ هذه الميزة أكثر مرونة من ذلك.
في هذا الدليل، سنلقي نظرة على كيفية استخدام Codable لربط البيانات من Cloud Firestore بأنواع Swift والعكس.
عند جلب مستند من Cloud Firestore، سيتلقّى تطبيقك معجمًا من أزواج المفاتيح/القيم (أو صفيفًا من القواميس، إذا كنت تستخدم إحدى العمليات التي تعرض مستندات متعددة).
يمكنك الآن مواصلة استخدام القواميس مباشرةً في Swift، وهي توفّر بعض المرونة الكبيرة التي قد تكون مطلوبة تمامًا في حالة الاستخدام. ومع ذلك، لا يضمن هذا النهج سلامة النوع، ومن السهل إدخال أخطاء يصعب تتبُّعها عن طريق إغفال كتابة أسماء السمات بشكل صحيح أو عدم ربط السمة الجديدة التي أضافها فريقك عند طرح هذه الميزة الجديدة المثيرة الأسبوع الماضي.
في السابق، عالج العديد من المطوّرين هذه العيوب من خلال تنفيذ طبقة ربط بسيطة سمحت لهم بربط القواميس بأنواع Swift. ولكن مرة أخرى، تستند معظم عمليات التنفيذ هذه إلى تحديد التعيين بين مستندات Cloud Firestore والأنواع المقابلة لنموذج بيانات تطبيقك يدويًا.
بفضل توافق Cloud Firestore مع واجهة برمجة التطبيقات Codable API في Swift، يصبح هذا الإجراء أسهل بكثير:
- ولن يكون عليك بعد الآن تنفيذ أي رمز ربط يدويًا.
- من السهل تحديد كيفية ربط السمات بأسماء مختلفة.
- وتتوفّر فيه ميزات مدمجة تتوافق مع العديد من أنواع Swift.
- ومن السهل إضافة إمكانية ربط الأنواع المخصّصة.
- والأفضل من ذلك، بالنسبة إلى نماذج البيانات البسيطة، لن يكون عليك كتابة أي رمز برمجي للربط على الإطلاق.
تعيين البيانات
تخزِّن Cloud Firestore البيانات في مستندات تربط المفاتيح بالقيم. لجلب data من مستند فردي، يمكننا استدعاء DocumentSnapshot.data()
، الذي يعرض قاموسًا يربط أسماء الحقول بـ Any
:
func data() -> [String : Any]?
.
وهذا يعني أنّه يمكننا استخدام بنية العنوان الفرعي في Swift للوصول إلى كل حقل فردي.
import FirebaseFirestore
#warning("DO NOT MAP YOUR DOCUMENTS MANUALLY. USE CODABLE INSTEAD.")
func fetchBook(documentId: String) {
let docRef = db.collection("books").document(documentId)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
let id = document.documentID
let data = document.data()
let title = data?["title"] as? String ?? ""
let numberOfPages = data?["numberOfPages"] as? Int ?? 0
let author = data?["author"] as? String ?? ""
self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author)
}
}
}
}
على الرغم من أنّه قد يبدو بسيطًا وسهل التنفيذ، إلا أنّ هذا الرمز غير مستقر، ويصعب الحفاظ عليه، ويكون عرضة للأخطاء.
كما ترى، نضع افتراضات حول أنواع البيانات في حقول المستند. قد تكون هذه المعلومات صحيحة أو غير صحيحة.
تذكَّر أنّه بما أنّه لا يتوفّر مخطّط، يمكنك بسهولة إضافة مستند جديد
إلى المجموعة واختيار نوع مختلف لحقل. قد تختار
سلسلة عن طريق الخطأ للحقل numberOfPages
، ما قد يؤدي
إلى حدوث مشكلة في الربط يصعب العثور عليها. وعليك أيضًا تعديل رمز الربط
عند إضافة حقل جديد، ما يشكّل عملية صعبة إلى حدٍ ما.
ولن ننسى أنّنا لا نستفيد من نظام تحديد النوع الدقيق في Swift، والذي يعرف النوع الصحيح لكل خاصية من سمات
Book
.
ما هو Codable؟
وفقًا لمستندات Apple، فإنّ Codable هو "نوع يمكنه تحويل نفسه إلى تمثيل خارجي والعكس". في الواقع، Codable هو بديل نوع لبروتوكولَي 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
نوع يحتوي على المستند المرتبط، أو خطأ في حال تعذّر 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 API تلقائيًا أسماء السمات لأنواع Swift من أجل تحديد أسماء السمات في مستندات Cloud Firestore التي نحاول ربطها. وبالتالي، ما دامت أسماء السمات متطابقة، ليس من الضروري إضافة
CodingKeys
إلى الأنواع القابلة للترميز. ومع ذلك، بعد استخدام CodingKeys
لنوع
معيّن، علينا إضافة جميع أسماء المواقع التي نريد ربطها.
في مقتطف الرمز أعلاه، حدّدنا خاصية id
التي قد نريد
استخدامها كمعرّف في عرض List
في SwiftUI. إذا لم نحدّده في
CodingKeys
، لن يتمّ ربطه عند جلب البيانات، وبالتالي سيصبح nil
.
سيؤدي ذلك إلى ملء عرض List
بالمستند الأول.
سيتم تجاهل أيّ خاصيّة غير مُدرَجة كحالة في CodingKeys
enum
المعنيّة أثناء عملية الربط. يمكن أن يكون هذا مفيدًا إذا أردنا تحديدًا استبعاد بعض المواقع من عملية الربط.
على سبيل المثال، إذا أردنا استبعاد السمة reasonWhyILoveThis
من
التعيين، ما علينا سوى إزالتها من التعداد CodingKeys
:
struct ProgrammingLanguage: Identifiable, Codable {
@DocumentID var id: String?
var name: String
var year: Date
var reasonWhyILoveThis: String = ""
enum CodingKeys: String, CodingKey {
case id
case name = "language_name"
case year
}
}
قد نحتاج أحيانًا إلى كتابة سمة فارغة مرة أخرى في مستند
Cloud Firestore. تتضمّن Swift مفهوم القيم الاختيارية للإشارة إلى عدم توفّر قيمة، ويتوافق Cloud Firestore مع قيم null
أيضًا.
ومع ذلك، فإنّ السلوك التلقائي لترميز الحقول الاختيارية التي تحتوي على قيمة nil
هو
تجاهلها فقط. يمنحنا العنصر@ExplicitNull
بعض التحكّم في كيفية معالجة Swift للعناصر
الاختيارية عند ترميزها: من خلال وضع علامة على سمة اختيارية على أنّها
@ExplicitNull
، يمكننا أن نطلب من Cloud Firestore كتابة هذه السمة في
الوثيقة بقيمة فارغة إذا كانت تحتوي على قيمة nil
.
استخدام برنامجَي ترميز وفك ترميز مخصّصَين لربط الألوان
في ما يلي موضوعنا الأخير في تغطية ربط البيانات باستخدام Codable، وهو مناقشة مُشفِّرات ومُشفِّرات عكسية مخصّصة. لا يتناول هذا القسم نوع بيانات Cloud Firestore الأصلي، ولكنّ برامج الترميز وفك الترميز المخصّصة مفيدة على نطاق واسع في تطبيقات Cloud Firestore.
"كيف يمكنني ربط الألوان" هو أحد الأسئلة الأكثر شيوعًا التي يطرحها المطوّرون، ليس فقط في ما يتعلّق بتطبيق Cloud Firestore، ولكن أيضًا في ما يتعلّق بالربط بين Swift وJSON. تتوفّر العديد من الحلول، ولكن معظمها تركّز على تنسيق JSON، ويحدّد كلّها تقريبًا الألوان كقاموس متداخل يتألّف من مكوّنات RGB.
يبدو أنّ هناك حلًا أفضل وأسهل. لماذا لا نستخدم ألوان الويب (أو على وجه التحديد، ترميز الألوان الست عشرية في CSS)؟ إنّها سهلة الاستخدام (وهي في الأساس سلسلة)، كما تتيح استخدام الشفافية.
لنتمكّن من ربط Color
في Swift بقيمته الست عشرية، علينا إنشاء إضافة
Swift تضيف Codable إلى Color
.
extension Color {
init(hex: String) {
let rgba = hex.toRGBA()
self.init(.sRGB,
red: Double(rgba.r),
green: Double(rgba.g),
blue: Double(rgba.b),
opacity: Double(rgba.alpha))
}
//... (code for translating between hex and RGBA omitted for brevity)
}
extension Color: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let hex = try container.decode(String.self)
self.init(hex: hex)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(toHex)
}
}
باستخدام decoder.singleValueContainer()
، يمكننا فك ترميز String
إلى Color
المكافئ له، بدون الحاجة إلى تداخل مكوّنات RGBA. بالإضافة إلى ذلك، يمكنك استخدام هذه القيم في واجهة مستخدم الويب لتطبيقك بدون الحاجة إلى تحويلها أولاً.
باستخدام هذا الرمز، يمكننا تعديل رمز ربط العلامات، ما يسهّل التعامل مع ألوان العلامات مباشرةً بدلاً من الحاجة إلى ربطها يدويًا في رمز واجهة المستخدم في تطبيقنا:
struct Tag: Codable, Hashable {
var title: String
var color: Color
}
struct BookWithTags: Codable {
@DocumentID var id: String?
var title: String
var numberOfPages: Int
var author: String
var tags: [Tag]
}
معالجة الأخطاء
في المقتطفات أعلاه من الرموز البرمجية، حاولنا الحدّ من معالجة الأخطاء إلى الحدّ الأدنى، ولكن في التطبيق العلني، عليك التأكّد من معالجة أي أخطاء بشكل سلس.
في ما يلي مقتطف رمز برمجي يعرض كيفية استخدام أي حالات خطأ قد تواجهها:
class MappingSimpleTypesViewModel: ObservableObject {
@Published var book: Book = .empty
@Published var errorMessage: String?
private var db = Firestore.firestore()
func fetchAndMap() {
fetchBook(documentId: "hitchhiker")
}
func fetchAndMapNonExisting() {
fetchBook(documentId: "does-not-exist")
}
func fetchAndTryMappingInvalidData() {
fetchBook(documentId: "invalid-data")
}
private func fetchBook(documentId: String) {
let docRef = db.collection("books").document(documentId)
docRef.getDocument(as: Book.self) { result in
switch result {
case .success(let book):
// A Book value was successfully initialized from the DocumentSnapshot.
self.book = book
self.errorMessage = nil
case .failure(let error):
// A Book value could not be initialized from the DocumentSnapshot.
switch error {
case DecodingError.typeMismatch(_, let context):
self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.valueNotFound(_, let context):
self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.keyNotFound(_, let context):
self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.dataCorrupted(let key):
self.errorMessage = "\(error.localizedDescription): \(key)"
default:
self.errorMessage = "Error decoding document: \(error.localizedDescription)"
}
}
}
}
}
التعامل مع الأخطاء في آخر الأخبار
يوضّح مقتطف الرمز السابق كيفية معالجة الأخطاء عند جلب مستند واحد. بالإضافة إلى جلب البيانات مرة واحدة، تتيح Cloud Firestore أيضًا إرسال التعديلات إلى تطبيقك فور حدوثها، وذلك باستخدام ما يُعرف باسم مستمعي الملخّصات: يمكننا تسجيل مستمع للملخّص في مجموعة (أو طلب بحث)، وستطلب Cloud Firestore من المستمع إجراء ذلك عند توفّر تعديل.
في ما يلي مقتطف رمز يوضّح كيفية تسجيل مستمع لقطة، وربط البيانات باستخدام Codable، ومعالجة أي أخطاء قد تحدث. ويوضّح أيضًا كيفية إضافة مستند جديد إلى المجموعة. كما سترى، ليس من الضروري تعديل الشبكة المحلية التي تحتوي على المستندات المُعرَّفة بأنفسنا، لأنّ الرمز البرمجي في مستمع اللقطات هو الذي يعتني بذلك.
class MappingColorsViewModel: ObservableObject {
@Published var colorEntries = [ColorEntry]()
@Published var newColor = ColorEntry.empty
@Published var errorMessage: String?
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
public func unsubscribe() {
if listenerRegistration != nil {
listenerRegistration?.remove()
listenerRegistration = nil
}
}
func subscribe() {
if listenerRegistration == nil {
listenerRegistration = db.collection("colors")
.addSnapshotListener { [weak self] (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
self?.errorMessage = "No documents in 'colors' collection"
return
}
self?.colorEntries = documents.compactMap { queryDocumentSnapshot in
let result = Result { try queryDocumentSnapshot.data(as: ColorEntry.self) }
switch result {
case .success(let colorEntry):
if let colorEntry = colorEntry {
// A ColorEntry value was successfully initialized from the DocumentSnapshot.
self?.errorMessage = nil
return colorEntry
}
else {
// A nil value was successfully initialized from the DocumentSnapshot,
// or the DocumentSnapshot was nil.
self?.errorMessage = "Document doesn't exist."
return nil
}
case .failure(let error):
// A ColorEntry value could not be initialized from the DocumentSnapshot.
switch error {
case DecodingError.typeMismatch(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.valueNotFound(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.keyNotFound(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.dataCorrupted(let key):
self?.errorMessage = "\(error.localizedDescription): \(key)"
default:
self?.errorMessage = "Error decoding document: \(error.localizedDescription)"
}
return nil
}
}
}
}
}
func addColorEntry() {
let collectionRef = db.collection("colors")
do {
let newDocReference = try collectionRef.addDocument(from: newColor)
print("ColorEntry stored with new document reference: \(newDocReference)")
}
catch {
print(error)
}
}
}
جميع مقتطفات الرموز البرمجية المستخدَمة في هذه المشاركة هي جزء من نموذج تطبيق يمكنك تنزيله من مستودع GitHub هذا.
ننصحك باستخدام Codable.
توفّر واجهة برمجة التطبيقات Codable API في Swift طريقة فعّالة ومرنة لربط البيانات من التنسيقات المُسلسلة إلى نموذج بيانات تطبيقاتك ومنه. في هذا الدليل، تعرّفت على مدى سهولة استخدامه في التطبيقات التي تستخدم Cloud Firestore كمستودع بيانات لها.
بدءًا من مثال أساسي يتضمّن أنواع بيانات بسيطة، زادنا بشكل تدريجي مدى تعقيد نموذج البيانات، مع الاعتماد في الوقت نفسه على Codable وتنفيذ Firebase لإجراء عملية الربط نيابةً عنا.
لمزيد من التفاصيل حول Codable، ننصحك بالاطّلاع على المراجع التالية:
- نشر جون ساندل مقالة رائعة عن أساسيات Codable.
- إذا كنت تفضّل الكتب، يمكنك الاطّلاع على دليل Flight School Guide to Swift Codable الذي كتبه "مات".
- وأخيرًا، لدى "دوني وولز" سلسلة كاملة حول Codable.
على الرغم من أنّنا بذلنا قصارى جهدنا لإعداد دليل شامل لربط مستندات Cloud Firestore، إلا أنّ هذا الدليل ليس شاملاً، وقد تستخدم استراتيجيات أخرى لربط أنواعك. باستخدام الزر إرسال ملاحظات أدناه، أخبِرنا بالخطط التي تستخدمها لربط أنواع أخرى من بيانات Cloud Firestore أو تمثيل البيانات في Swift.
لا يوجد سبب لعدم استخدام ميزة Codable في Cloud Firestore.