文字っぽいの。

文字を書いています。写真も混ざります。

Swift4のCodableをUIImageに対応させる

環境

  • 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は、DecodableEncodableの2つのprotocolを要求するprotocolであるため、DecodableEncodableのそれぞれに準拠すれば良いです。

ここで、UIImageが邪魔になってきます。IntStringDecodable / 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)
        }
    }
}

このサンプルコードでは、DecodableEncodableをわざわざ分けて書いていますが、

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