環境
前提
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 {
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)
}
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
を拡張し、画像フォーマットを切り替えてあげると便利です。もちろん用途によりますが。