【Swift】Tips: あると便利だったextension達(UIImage編)

2018.01.22

はじめに

モバイルアプリサービス部の中安です。

アプリを作ってきた際に、あるとなかなか便利だったextensionをボチボチとご紹介していければと思っています。 アプリの目的に合わせてやりかえる必要があったり、もっとよい実装があるかもしれませんが、何かの役に立てば光栄です。

今回は UIImage 編です。

コンテキスト描画のラップ

画像を加工する際の常套手段として、グラフィックコンテキストに描画してUIImageを取得する方法があると思います。 文章では分かりづらいので、ソースコードで書くと

// コンテキストを開始
let size = CGSize(width: 100, height: 100)
UIGraphicsBeginImageContext(size)
// コンテキストを取得
let context = UIGraphicsGetCurrentContext()!

(何かしらのコンテキストへの描画)

// コンテキストに描画した画像を取得
let image = UIGraphicsGetImageFromCurrentImageContext()!
// コンテキストの終了
UIGraphicsEndImageContext()

(何かしらのコンテキストへの描画) の部分に画像のリサイズや切り出し、回転、合成、その他もろもろの処理を書いて画像を新たに取得します。

まぁ、この共通な部分を毎回毎回書くのは煩わしいということで、以下のような extension を用意します。

extension UIImage {
    
    class func imageFromContext(_ size: CGSize, drawing: (CGContext)->Void) -> UIImage {
        UIGraphicsBeginImageContext(size)
        drawing(UIGraphicsGetCurrentContext()!)
        let ret = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return ret!
    }
}

すると、例えば画像を任意のサイズにリサイズするメソッドを作りたいと思った時に下記のように実装できます。

extension UIImage {

    func resized(to size: CGSize) -> UIImage {
        return UIImage.imageFromContext(size) { _ in
            draw(in: CGRect(origin: .zero, size: size))
        }
    }
}

// 使用ケース
let originalImage = UIImage(named: "hoge")!
let resizedImage = originalImage.resized(to: CGSize(width: 100, height: 100))

上記例では _ in として省略してますが、コンテキストにパスを描いたりするなどするときには context in と書き換えて、そのCGContextの変数に描いていくこともできます。

画像を色々と扱うようなアプリでは、こういうものを用意しておけば便利になります。

画像ファイルパスで画像の取得/保存

アプリにバンドルされている画像リソースの場合は、UIImage(named:) メソッドで取得することができますが、画像の書き込みが必要である場合など、そうではないケースの時は読み書き可能なアプリのディレクトリ(たとえばドキュメントディレクトリなど)に置いて画像リソースを扱うことになるかと思います。

そのとき、UIImage を Data オブジェクトに変換したり、Data を UIImage に変換するなどの処理を挟まなければなりません。

まぁ、これも毎回やるのが煩わしいので、ファイルの置き場所のパスだけでやりとりさせるように extension をしてあげると便利です。

画像の取得

ファイルパスを渡して初期化できるコンビニエンスなイニシャライザを用意してあげます。

extension UIImage {
    
    convenience init?(path: String) {
        guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
            return nil
        }
        self.init(data: data)
    }
}

渡したファイルパスにファイルが存在するとは限らないので、オプショナルなイニシャライザにしてあげることもミソです。

画像の保存

指定したファイルパスに画像を簡単に保存してやるメソッドを用意してやります。

extension UIImage {
    
    func write(to path: String) {
        let data = UIImagePNGRepresentation(self)
        try? data?.write(to: URL(fileURLWithPath: path), options: [.atomic])
    }
}

ただし、ここは悩みどころです。

この例ではPNGに限定していますが、アプリによってはPNGだけではなく、JPEGでの保存の要求があるかもしれません。その場合はこのような感じでしょうか。

extension UIImage {
    
    func write(asPngTo path: String) {
        let data = UIImagePNGRepresentation(self)
        try? data?.write(to: URL(fileURLWithPath: path), options: [.atomic])
    }
    
    func write(asJpegTo path: String, compressionQuality quality: CGFloat = 0.9) {
        let data = UIImageJPEGRepresentation(self, quality)
        try? data?.write(to: URL(fileURLWithPath: path), options: [.atomic])
    }
}

また、もうひとつ悩みどころとしては、保存の失敗をキャッチする仕組みも必要かもしれません。

extension UIImage {
    
    func write(to path: String) throws {
        let data = UIImagePNGRepresentation(self)
        try data?.write(to: URL(fileURLWithPath: path), options: [.atomic])
    }
}

こんな感じに throws をつけて、例外を外出しする感じでしょうか。この辺りはアプリの設計方針に応じて書き換えが必要かと思います。