التحليل والعمل مع Codable في Swift 4



اكتسب تنسيق JSON شعبية كبيرة ، وعادة ما يتم استخدامه لنقل البيانات وتنفيذ الاستعلام في تطبيقات خادم العميل. يتطلب تحليل JSON أدوات تشفير / فك تشفير بهذا التنسيق ، وقد قامت Apple بتحديثها مؤخرًا. في هذه المقالة ، سنلقي نظرة على طرق تحليل JSON باستخدام بروتوكول فك التشفير ، ومقارنة البروتوكول القابل للتشفير الجديد مع سلفه NSCoding ، وتقييم المزايا والعيوب ، وتحليل كل شيء بأمثلة محددة ، وأيضًا النظر في بعض الميزات التي تمت مواجهتها في تنفيذ البروتوكولات.


ما هو كودابل؟

في WWDC2017 ، جنبًا إلى جنب مع الإصدار الجديد من Swift 4 ، قدمت Apple أدوات تشفير / فك تشفير جديدة للبيانات يتم تنفيذها بواسطة البروتوكولات الثلاثة التالية:

- قابل للترميز
- ترميز
- فك شفرة

في معظم الحالات ، يتم استخدام هذه البروتوكولات للعمل مع JSON ، ولكن بالإضافة إلى ذلك يتم استخدامها أيضًا لحفظ البيانات على القرص ، ونقلها عبر الشبكة ، إلخ. يستخدم Encodable لتحويل هياكل بيانات Swift إلى كائنات JSON ، بينما يساعد Decodable ، على العكس من ذلك ، على تحويل كائنات JSON إلى نماذج بيانات Swift. يجمع البروتوكول القابل للتشفير بين الاثنين السابقين وهو أنماطهم:

typealias Codable = Encodable & Decodable 


لتتوافق مع هذه البروتوكولات ، يجب أن تطبق أنواع البيانات الطرق التالية:

ترميز
encode (to :) - يشفر نموذج البيانات إلى نوع التشفير المحدد

فك الشفرة
الحرف الأول (من :) - تهيئة نموذج البيانات من وحدة فك الترميز المقدمة

قابل للترميز
ترميز (إلى :)
الحرف الأول (من :)

حالة استخدام بسيطة

ضع في اعتبارك الآن مثالًا بسيطًا على استخدام Codable ، نظرًا لأنه يطبق كلاً من Encodable و Decodable ، في هذا المثال يمكنك أن ترى على الفور جميع وظائف البروتوكول. لنفترض أن لدينا أبسط بنية بيانات JSON:

 { "title": "Nike shoes", "price": 10.5, "quantity": 1 } 


سيبدو نموذج البيانات للعمل مع JSON هذا كما يلي:
 struct Product: Codable { var title:String var price:Double var quantity:Int enum CodingKeys: String, CodingKey { case title case price case quantity } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(price, forKey: .price) try container.encode(quantity, forKey: .quantity) } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) title = try container.decode(String.self, forKey: .title) price = try container.decode(Double.self, forKey: .price) quantity = try container.decode(Int.self, forKey: .quantity) } } 


يتم تنفيذ كلتا الطريقتين الضروريتين ، كما يتم وصف التعداد لتحديد قائمة حقول التشفير / فك التشفير. في الواقع ، يمكن تبسيط الكتابة بشكل كبير لأن Codable يدعم التوليد التلقائي للترميز (إلى :) و init (من :) الأساليب ، بالإضافة إلى التعداد اللازم. أي ، في هذه الحالة ، يمكنك كتابة الهيكل على النحو التالي:

 struct Product: Codable { var title:String var price:Double var quantity:Int } 


بسيطة للغاية وأضيق الحدود. فقط ، لا تنس أن مثل هذا التسجيل المختصر لن يعمل إذا:

- يختلف هيكل نموذج البيانات الخاص بك عن الذي تريد تشفيره / فك تشفيره

- قد تحتاج إلى ترميز / فك تشفير الخصائص الإضافية إلى جانب خصائص نموذج البيانات الخاص بك

- قد لا تدعم بعض خصائص نموذج بياناتك البروتوكول القابل للتشفير. في هذه الحالة ، ستحتاج إلى تحويلها من / إلى بروتوكول قابل للتشفير

- في حالة عدم تطابق أسماء المتغيرات في نموذج البيانات وأسماء الحقول في الحاوية

نظرًا لأننا قد نظرنا بالفعل في أبسط تعريف لنموذج البيانات ، فمن المفيد إعطاء مثال صغير لاستخدامه العملي:

لذا ، في سطر واحد ، يمكنك تحليل استجابة الخادم بتنسيق JSON:
 let product: Product = try! JSONDecoder().decode(Product.self, for: data) 


وعلى النقيض من ذلك ، فإن التعليمات البرمجية التالية ستقوم بإنشاء كائن JSON من نموذج البيانات:
 let productObject = Product(title: "Cheese", price: 10.5, quantity: 1) let encodedData = try? JSONEncoder().encode(productObject) 


كل شيء مريح وسريع للغاية. بعد وصف نماذج البيانات بشكل صحيح وجعلها قابلة للتشفير ، يمكنك ترميز / فك تشفير البيانات في سطر واحد. لكننا اعتبرنا أبسط نموذج بيانات يحتوي على عدد صغير من الحقول من نوع بسيط. فكر في المشاكل المحتملة:

ليست كل الحقول في نموذج البيانات قابلة للتشفير.

لكي يقوم نموذج البيانات الخاص بك بتنفيذ البروتوكول القابل للتشفير ، يجب أن تدعم جميع حقول النموذج هذا البروتوكول. بشكل افتراضي ، يدعم بروتوكول Codable أنواع البيانات التالية: String ، Int ، Double ، Data ، URL . يدعم Codable أيضًا Array ، Dictionary ، اختياري ، ولكن فقط إذا كانت تحتوي على أنواع قابلة للتشفير . إذا كانت بعض خصائص نموذج البيانات لا تتوافق مع الشفرة ، فيجب إحضارها إليها.

 struct Pet: Codable { var name: String var age: Int var type: PetType enum CodingKeys: String, CodingKey { case name case age case type } init(from decoder: Decoder) throws { . . . } func encode(to encoder: Encoder) throws { . . . } } 


إذا استخدمنا في نموذج البيانات القابلة للتشفير نوعًا مخصصًا ، على سبيل المثال ، مثل PetType ، وأردت ترميزه / فك تشفيره ، فيجب عليه أيضًا تنفيذ التهيئة والتشفير أيضًا.

لا يتطابق نموذج البيانات مع حقول JSON

إذا تم تحديد الحقول 3 في نموذج البيانات الخاص بك ، على سبيل المثال ، وفي كائن JSON ، حصلت على 5 حقول ، اثنان منها إضافيان إلى تلك الحقول الثلاثة ، فلن يتغير أي شيء في التحليل ، فأنت تحصل على 3 حقول فقط من تلك 5. إذا حدث العكس الوضع وفي كائن JSON سيكون هناك حقل واحد على الأقل من نموذج البيانات ، سيحدث خطأ في وقت التشغيل.
إذا كانت بعض الحقول قد تكون اختيارية وغائبة دوريًا في كائن JSON ، فمن الضروري في هذه الحالة جعلها اختيارية:

 class Product: Codable { var id: Int var productTypeId: Int? var art: String var title: String var description: String var price: Double var currencyId: Int? var brandId: Int var brand: Brand? } 


استخدام هياكل JSON أكثر تعقيدًا

غالبًا ما تكون استجابة الخادم عبارة عن مجموعة من الكيانات ، أي أنك تطلب ، على سبيل المثال ، قائمة بالمتاجر والحصول على إجابة في النموذج:

 { "items": [ { "id": 1, "title": " ", "link": "https://www.youtube.com/watch?v=Myp6rSeCMUw", "created_at": 1497868174, "previewImage": "http://img.youtube.com/vi/Myp6rSeCMUw/mqdefault.jpg" }, { "id": 2, "title": "  2", "link": "https://www.youtube.com/watch?v=wsCEuNJmvd8", "created_at": 1525952040, "previewImage": "http://img.youtube.com/vi/wsCEuNJmvd8/mqdefault.jpg" } ] } 

في هذه الحالة ، يمكنك كتابته وفك ترميزه ببساطة كمجموعة من كيانات Shop .

 struct ShopListResponse: Decodable { enum CodingKeys: String, CodingKey { case items } let items: [Shop] } 


في هذا المثال ، ستعمل وظيفة التهيئة التلقائية ، ولكن إذا كنت تريد كتابة فك التشفير بنفسك ، فستحتاج إلى تحديد النوع الذي تم فك ترميزه كمصفوفة:

 self.items = try container.decode([Shop].self, forKey: .items) 


يجب أن تنفذ بنية المتجر أيضًا بروتوكول فك التشفير ، على التوالي.

 struct Shop: Decodable { var id: Int? var title: String? var address: String? var shortAddress: String? var createdAt: Date? enum CodingKeys: String, CodingKey { case id case title case address case shortAddress = "short_address" case createdAt = "created_at" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try? container.decode(Int.self, forKey: .id) self.title = try? container.decode(String.self, forKey: .title) self.address = try? container.decode(String.self, forKey: .address) self.shortAddress = try? container.decode(String.self, forKey: .shortAddress) self.createdAt = try? container.decode(Date.self, forKey: .createdAt) } } 


سيبدو تحليل مجموعة العناصر هذه كما يلي:

 let parsedResult: ShopListResponse = try? JSONDecoder().decode(ShopListResponse.self, from: data) 


وبالتالي ، يمكنك العمل بسهولة مع صفائف نماذج البيانات واستخدامها داخل نماذج أخرى.

تنسيق التاريخ

في هذا المثال ، هناك فارق بسيط آخر ، هنا واجهنا أولاً استخدام نوع التاريخ . عند استخدام هذا النوع ، قد تكون هناك مشاكل في ترميز التاريخ ، وعادة ما تكون هذه المشكلة متسقة مع الواجهة الخلفية. التنسيق الافتراضي هو .deferToDate :

 struct MyDate : Encodable { let date: Date } let myDate = MyDate(date: Date()) try! encoder.encode(foo) 


سيبدو myDate كما يلي:
 { "date" : 519751611.12542897 } 


إذا احتجنا إلى استخدام تنسيق .iso8601 ، على سبيل المثال ، يمكننا بسهولة تغيير التنسيق باستخدام خاصية dateEncodingStr Strategy :

 encoder.dateEncodingStrategy = .iso8601 


الآن سيبدو التاريخ كما يلي:

 { "date" : "2017-06-21T15:29:32Z" } 

يمكنك أيضًا استخدام تنسيق تاريخ مخصص أو حتى كتابة وحدة فك ترميز التاريخ الخاصة بك باستخدام خيارات التنسيق التالية:

.formatted (DateFormatter) - تنسيق وحدة فك ترميز التاريخ الخاصة بها
.custom (يرمي ((Date، Encoder) -> Void) - قم بإنشاء تنسيق فك تشفير التاريخ الخاص بك بالكامل

تحليل الكائنات المتداخلة

لقد درسنا بالفعل كيف يمكنك استخدام نماذج البيانات داخل النماذج الأخرى ، ولكن من الضروري في بعض الأحيان تحليل حقول JSON المضمنة في الحقول الأخرى دون استخدام نموذج بيانات منفصل. ستكون المشكلة أكثر وضوحًا إذا أخذناها في الاعتبار كمثال. لدينا JSON التالية:

 { "id": 349, "art": "M0470500", "title": "- Vichy 50 ", "ratings": { "average_rating": 4.1034, "votes_count": 29 } } 


نحتاج إلى تحليل الحقلين "متوسط" و "votes_count" ، ويمكن حل ذلك بطريقتين ، إما إنشاء نموذج بيانات التقييمات بحقلين وحفظ البيانات فيه ، أو يمكنك استخدام nestedContainer . لقد ناقشنا بالفعل الحالة الأولى ، وسيبدو استخدام الحالة الثانية كما يلي:

 class Product: Decodable { var id: Int var art: String? var title: String? var votesCount: Int var averageRating: Double enum CodingKeys: String, CodingKey { case id case art case title case ratings } enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(Int.self, forKey: .id) self.art = try? container.decode(String.self, forKey: .art) self.title = try? container.decode(String.self, forKey: .title) // Nested ratings let ratingsContainer = try container.nestedContainer(keyedBy: RatingsCodingKeys.self, forKey: .ratings) self.votesCount = try ratingsContainer.decode(Int.self, forKey: .votesCount) self.averageRating = try ratingsContainer.decode(Double.self, forKey: .averageRating) } } 


أي ، يتم حل هذه المشكلة عن طريق إنشاء حاوية إضافية أخرى باستخدام nestedContainter وتحليلها الإضافي. هذا الخيار مناسب إذا لم يكن عدد الحقول المتداخلة كبيرًا جدًا ، وإلا فمن الأفضل استخدام نموذج بيانات إضافي.

عدم تطابق أسماء حقول JSON وخصائص نموذج البيانات

إذا انتبهت لكيفية تعريف التعدادات في نماذج البيانات لدينا ، يمكنك أن ترى أن عناصر التعداد يتم تخصيصها أحيانًا سلسلة تغير القيمة الافتراضية ، على سبيل المثال:

 enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" } 


يتم ذلك من أجل مطابقة أسماء متغيرات النموذج وحقول JSON بشكل صحيح. هذا مطلوب عادةً للحقول التي يتكون اسمها من عدة كلمات ، وفي JSON يتم فصلها بشرطة سفلية. من حيث المبدأ ، فإن إعادة تعريف التعداد هي الأكثر شيوعًا وتبدو بسيطة ، ولكن حتى ذلك الحين توصلت Apple إلى حل أكثر أناقة. يمكن حل هذه المشكلة في سطر واحد باستخدام keyDecodingStrategy . ظهرت هذه الميزة في Swift 4.1

لنفترض أن لديك JSON من النموذج:

 let jsonString = """ [ { "name": "MacBook Pro", "screen_size": 15, "cpu_count": 4 }, { "name": "iMac Pro", "screen_size": 27, "cpu_count": 18 } ] """ let jsonData = Data(jsonString.utf8) 


لنقم بإنشاء نموذج بيانات لذلك:

 struct Mac: Codable { var name: String var screenSize: Int var cpuCount: Int } 

يتم تسجيل المتغيرات في النموذج وفقًا للاتفاقية ، وتبدأ بحرف صغير ثم تبدأ كل كلمة بحرف كبير (يسمى camelCase ). ولكن في JSON ، تُكتب الحقول بشرطة سفلية (ما يسمى ب snake_case ). الآن ، من أجل نجاح التحليل ، نحتاج إما إلى تحديد التعداد في نموذج البيانات الذي سنحدد فيه مراسلات أسماء حقول JSON مع أسماء المتغيرات ، أو سنحصل على خطأ وقت التشغيل. ولكن من الممكن الآن تحديد keyDecodingStrategy

 let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { let macs = try decoder.decode([Mac].self, from: jsonData) } catch { print(error.localizedDescription) } 


بالنسبة إلى وظيفة التشفير ، يمكنك استخدام التحويل العكسي وفقًا لذلك:

 encoder.keyEncodingStrategy = .convertToSnakeCase 


من الممكن أيضًا تخصيص keyDecodingStr Strategy باستخدام الإغلاق التالي:

 let jsonDecoder = JSONDecoder() jsonDecoder.keyDecodingStrategy = .custom { keys -> CodingKey in let key = keys.last!.stringValue.split(separator: "-").joined() return PersonKey(stringValue: String(key))! } 


يسمح هذا الإدخال ، على سبيل المثال ، باستخدام الفاصل "-" لـ JSON. مثال على JSON المستخدم:

 { "first-Name": "Taylor", "last-Name": "Swift", "age": 28 } 

وبالتالي ، يمكن تجنب تعريف إضافي للعد في كثير من الأحيان.

معالجة الخطأ

عند تحليل JSON وتحويل البيانات من تنسيق إلى آخر ، تكون الأخطاء حتمية ، فلنلق نظرة على خيارات معالجة أنواع مختلفة من الأخطاء. عند فك التشفير ، يمكن حدوث أنواع الأخطاء التالية:

  • DecodingError.dataCorrupted (DecodingError.Context) - البيانات تالفة. عادةً ما يعني أن البيانات التي تحاول فك تشفيرها لا تتطابق مع التنسيق المتوقع ، على سبيل المثال ، بدلاً من تنسيق JSON المتوقع ، تلقيت تنسيقًا مختلفًا تمامًا.
  • DecodingError.keyNotFound (CodingKey، DecodingError.Context) - لم يتم العثور على الحقل المطلوب. يعني أن الحقل الذي توقعت تلقيه مفقود
  • DecodingError.typeMismatch (Any.Type، DecodingError.Context) - عدم تطابق النوع. عندما لا يتطابق نوع البيانات في النموذج مع نوع الحقل المستلم
  • DecodingError.valueNotFound (Any.Type، DecodingError.Context) - قيمة مفقودة لحقل معين. تعذر تهيئة الحقل الذي قمت بتحديده في نموذج البيانات ، ربما في البيانات المستلمة هذا الحقل لا شيء. يحدث هذا الخطأ فقط مع الحقول غير الاختيارية ، إذا لم يكن من الضروري أن يحتوي الحقل على قيمة ، فلا تنس أن تجعله اختياريًا.


عند تشفير البيانات ، من الممكن حدوث خطأ:

EncodingError.invalidValue (Any.Type، DecodingError.Context) - فشل في تحويل نموذج البيانات إلى تنسيق معين

مثال على معالجة الأخطاء عند تحليل JSON:

  do { let decoder = JSONDecoder() _ = try decoder.decode(businessReviewResponse.self, from: data) } catch DecodingError.dataCorrupted(let context) { print(DecodingError.dataCorrupted(context)) } catch DecodingError.keyNotFound(let key, let context) { print(DecodingError.keyNotFound(key,context)) } catch DecodingError.typeMismatch(let type, let context) { print(DecodingError.typeMismatch(type,context)) } catch DecodingError.valueNotFound(let value, let context) { print(DecodingError.valueNotFound(value,context)) } catch let error{ print(error) } 


من الأفضل بالطبع معالجة الأخطاء في وظيفة منفصلة ، ولكن هنا ، من أجل الوضوح ، يتم إجراء تحليل الأخطاء مع التحليل. على سبيل المثال ، سيظهر ناتج الخطأ إذا لم تكن هناك قيمة لحقل "المنتج" كما يلي:

الصورة

مقارنة بين الشفرة و NSCoding

بالطبع ، يعد البروتوكول القابل للتشفير خطوة كبيرة إلى الأمام في تشفير / فك تشفير البيانات ، ولكن بروتوكول NSCoding كان موجودًا قبله. دعنا نحاول مقارنتها ونرى ما هي فوائد Codable:

  • عند استخدام بروتوكول NSCoding ، يجب أن يكون الكائن فئة فرعية من NSObject ، مما يعني تلقائيًا أن نموذج بياناتنا يجب أن يكون فئة. في Codable ، ليست هناك حاجة إلى الميراث ، على التوالي ، يمكن أن يكون نموذج البيانات فئة وبنية وتعدادًا .
  • إذا كنت بحاجة إلى وظائف ترميز وفك تشفير منفصلة ، كما هو الحال ، على سبيل المثال ، في حالة تحليل بيانات JSON التي تم تلقيها من خلال واجهة برمجة التطبيقات ، يمكنك استخدام بروتوكول فك تشفير واحد فقط. أي أنه لا توجد حاجة لتنفيذ أساليب التهيئة أو التشفير غير الضرورية في بعض الأحيان.
  • يمكن للبرمجة إنشاء طرق التهيئة والتشفير المطلوبة تلقائيًا ، بالإضافة إلى تعداد CodingKeys الاختياري. هذا ، بالطبع ، يعمل فقط إذا كان لديك حقول بسيطة في بنية البيانات ، وإلا فسيكون هناك حاجة إلى تخصيص إضافي. في معظم الحالات ، خاصة بالنسبة لهياكل البيانات الأساسية ، يمكنك استخدام الإنشاء التلقائي ، خاصة إذا قمت بإعادة تعريف keyDecodingStrategy ، فهذا مناسب ويقلل من بعض التعليمات البرمجية غير الضرورية.


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

Source: https://habr.com/ru/post/ar414221/


All Articles