環境
- Xcode9 GM
- Swift4
前提
Swift4からCodableという便利Protocolが追加されました。
public protocol Encodable { public func encode(to encoder: Encoder) throws } public protocol Decodable { public init(from decoder: Decoder) throws } public typealias Codable = Decodable & Encodable
やりたいこと
UIImageをプロパティに持つstructをCodable
protocolに準拠させたい場合があります。
struct Photo { let id: Int let title: String let image: UIImage? }
前述した通りCodable
は、Decodable
とEncodable
の2つのprotocolを要求するprotocolであるため、Decodable
とEncodable
のそれぞれに準拠すれば良いです。
ここで、UIImage
が邪魔になってきます。Int
やString
はDecodable / Encodable
に対応していますが、UIImage
はしていません。なので、自分で処理を書く必要があります。
ちなみに、Custom TypeをCodableに対応させる方法はAppleのドキュメントにあるので、そちらを見ると良いです。
structで対応させる場合
こんな感じで、UIImageはbase64で文字列にエンコードしてあげます。簡単ですね。
extension Photo: Decodable { enum CodingKeys: String, CodingKey { case id case title case image } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) id = try values.decode(Int.self, forKey: .id) title = try values.decode(String.self, forKey: .title) let imageDataBase64String = try values.decode(String.self, forKey: .image) if let data = Data(base64Encoded: imageDataBase64String) { image = UIImage(data: data) } else { image = nil } } } extension Photo: Encodable { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encode(title, forKey: .title) if let image = image, let imageData = UIImagePNGRepresentation(image) { let imageDataBase64String = imageData.base64EncodedString() try container.encode(imageDataBase64String, forKey: .image) } } }
このサンプルコードでは、Decodable
とEncodable
をわざわざ分けて書いていますが、
extension Photo: Codable { }
とすれば、分けて書かなくて良くなります。ちなみに、UIImageそのものをCodableに対応させようとあれこれ試したのですが、うまくいきませんでした。強い人教えてくれ!
余談その1
structではなくclassをCodableに対応させる場合、すこしだけ記法が変わります。
final class Photo { let id: Int let title: String let image: UIImage? init(id: Int, title: String, image: UIImage?) { self.id = id self.title = title self.image = image } } extension Photo: Codable { // Decodable enum CodingKeys: String, CodingKey { case id case title case image } convenience init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) let id = try values.decode(Int.self, forKey: .id) let title = try values.decode(String.self, forKey: .title) let imageDataBase64String = try values.decode(String.self, forKey: .image) let image: UIImage? if let data = Data(base64Encoded: imageDataBase64String) { image = UIImage(data: data) } else { image = nil } self.init(id: id, title: title, image: image) } // Encodable func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encode(title, forKey: .title) if let image = image, let imageData = UIImagePNGRepresentation(image) { let imageDataBase64String = imageData.base64EncodedString() try container.encode(imageDataBase64String, forKey: .image) } } }
余談その2
上記サンプルコードではUIImagePNGRepresentation
を使って決め打ちで画像を変換していますが、
extension UIImage { private var hasAlpha: Bool { guard let alphaInfo = cgImage?.alphaInfo else { return false } switch alphaInfo { case .first, .last, .premultipliedFirst, .premultipliedLast, .alphaOnly: return true case .none, .noneSkipFirst, .noneSkipLast: return false } } var data: Data? { if hasAlpha { return UIImagePNGRepresentation(self) } else { return UIImageJPEGRepresentation(self, 1.0) } } }
のように UIImage
を拡張し、画像フォーマットを切り替えてあげると便利です。もちろん用途によりますが。