
اكتسب تنسيق 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)
أي ، يتم حل هذه المشكلة عن طريق إنشاء حاوية إضافية أخرى باستخدام
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 ، مع الحفاظ على سهولة الاستخدام. كما تتيح البروتوكولات تنظيم بنية الكود بشكل صحيح عن طريق فصل نماذج البيانات وطرق العمل معها في وحدات منفصلة.