Enum ونوع مخصص من نوع JSON بدائي
تعلّموا تقنيتين في Codable

سنتعلم اليوم حالتين قد تحتاجون إلى التعامل معهما عند العمل مع Codable: تحليل (parsing) الـ Enum، وتحليل نوع مخصص من نوع بدائي مثل string.
Enum
إذا كان للـ enum الخاص بكم raw value يمكن تمثيلها كـ Int أو String (وأظن أن هذا يغطي معظم الحالات)، فيمكننا جعل الـ enum متوافقًا مع Codable بإسناد raw value إليه.
لنفترض أن لديكم كائن JSON كهذا:
{"name": "iOS developer", "status": "open"}
lang-json
يمكنكم إنشاء struct في Swift كهذا:
<pre>struct Job: Codable {
enum Status: String, Codable {
case open
case close
}
let name: String
let status: Status
}
lang-swift
وإذا كان لديكم enum لا يمكن تمثيله كـ String أو Int، فلا يزال بإمكانكم جعله متوافقًا مع Codable ما دامت الـ raw value نفسها متوافقة مع Codable.
نوع مخصص من نوع بدائي
إذا كان لديكم نوع مخصص يمكن اشتقاقه من نوع JSON أساسي، فمثلًا لديكم كائن تُرسل فيه صورة على شكل string مرمّز بـ base64:
{
"name": "XXX",
"image": "data:image/png;base64,iVBORw0KGgo......."
}
lang-json
بما أن UIImage غير متوافق مع Codable ولا يمكننا جعله متوافقًا عبر extension، فلدينا طريقتان للتعامل مع ذلك:
- إنشاء wrapper class آخر حول هذه الصورة.
struct Base64ImageWrapper: Codable {
let image: UIImage
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let base64String = try container.decode(String.self)
let components = base64String.split(separator: ",")
if components.count != 2 {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Wrong data format")
}
let dataString = String(components[1])
if let dataDecoded = Data(base64Encoded: dataString, options: .ignoreUnknownCharacters),
let image = UIImage(data: dataDecoded) {
self.image = image
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Can't initialize image from data string")
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let data = image.jpegData(compressionQuality: 1)
let prefix = "data:image/jpeg;base64,"
guard let base64String = data?.base64EncodedString(options: .lineLength64Characters) else {
throw EncodingError.invalidValue(image, EncodingError.Context(codingPath: [], debugDescription: "Can't encode image to base64 string."))
}
try container.encode(prefix + base64String)
}
}
lang-swift
يُفترض أن يكون الكود أعلاه مألوفًا لكم، مع فكرة رئيسية واحدة. فبدلًا من:
let values = try decoder.container(keyedBy: CodingKeys.self)
lang-swift
نستخدم:
let container = try decoder.singleValueContainer()
lang-swift
لأننا نتعامل في هذه الحالة مع قيمة بدائية واحدة، وهي string بترميز base64. ثم يمكننا استخدام هذا الـ wrapper بهذه الطريقة:
<pre>struct ModelUsingImageWrapper: Codable {
let name: String
let image: Base64ImageWrapper
}
lang-swift
- إضافة encode/decode مخصص لـ UIImage في KeyedEncodingContainer:
extension KeyedEncodingContainer {
mutating func encode(_ value: UIImage,
forKey key: KeyedEncodingContainer.Key) throws {
let imageData = value.jpegData(compressionQuality: 1)
let prefix = "data:image/jpeg;base64,"
guard let base64String = imageData?.base64EncodedString(options: .lineLength64Characters) else {
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: "Can't encode image to base64 string."))
}
try encode(prefix + base64String, forKey: key)
}
}
extension KeyedDecodingContainer {
public func decode(_ type: UIImage.Type, forKey key: KeyedDecodingContainer.Key) throws -> UIImage {
let base64String = try decode(String.self, forKey: key)
// data:image/svg+xml;base64,PD.....
let components = base64String.split(separator: ",")
if components.count != 2 {
throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Unsupported format"))
}
let dataString = String(components[1])
if let dataDecoded = Data(base64Encoded: dataString, options: .ignoreUnknownCharacters), let image = UIImage(data: dataDecoded) {
return image
} else {
throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Unsupported format"))
}
}
}
lang-swift
ثم استخدامه مع encode وdecode صريحين.
struct ModelUsingKeyedEncodingContainer: Codable {
let name: String
let image: UIImage<pre> enum CodingKeys: String, CodingKey {
case name
case image
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
image = try container.decode(UIImage.self, forKey: .image)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(image, forKey: .image)
}
}
lang-swift
يُفترض أن تكفي هاتان الطريقتان لمعظم حالات الاستخدام لديكم.


