post Image
swiftで丸画像をパフォーマンス高く表示する方法

この記事は Retty Advent Calendar 21日目です。
昨日は @shinichi-nakagawa-r さんの REST API提供者と自分にやさしいAPIクライアントをPythonでいい感じに作る方法 でした。

はじめに

RettyのiOSエンジニア兼なんでも屋の櫻井です。
今日のネタはiOSでパフォーマンス高く丸画像を作るというお話になります。

この記事を書くキッカケとなったのは、直近でリリースしました弊社iOSアプリのRettyのリニューアルにおいて丸画像を多用してたらある画面で一瞬四角に表示されてから丸く表示されることがあったためです。

このとき丸画像を作る方法は一般的によく使われる方法ですが、 UIImageView の clipToBoundlayer.masksToBounds をtrueにして layer.cornerRadius を自分の高さ(または幅)の 1/2 を指定する方法を使っていました。

よくある実装風景

let imageView = UIImageView(image: UIimage(named: "imageName"))
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = self.frame.height / 2.0

self.view.addSubView(imageView)

swiftで丸画像をパフォーマンス高く表示する方法

具体的な実装

色々調べた結果、最初から丸く切り抜いた画像を使うことで動作は軽くなるし、表示が突如変わることもないよという情報をキャッチしました。
合わせて、正方形でない画像を入れたときの対応やstoryboard上から使いやすくするために今回はUIImageViewを継承した新しいクラスを作ることで対応しました。

実際のコードはこちらになります。

import UIKit

open class EnhancedCircleImageView: UIImageView {
    open override func awakeFromNib() {
        super.awakeFromNib()
        // swap UIImage because you can't asign yourself.
        let image = self.image
        self.image = image
    }

    open override var image: UIImage? {
        get { return super.image }
        set {
            self.contentMode = .scaleAspectFit
            super.image = newValue?.roundImage()
        }
    }

    open override func layoutSubviews() {
        super.layoutSubviews()
        self.layer.cornerRadius = self.frame.height / 2.0
    }
}

extension UIImage {
    func roundImage() -> UIImage {
        let minLength: CGFloat = min(self.size.width, self.size.height)
        let rectangleSize: CGSize = CGSize(width: minLength, height: minLength)
        UIGraphicsBeginImageContextWithOptions(rectangleSize, false, 0.0)

        UIBezierPath(roundedRect: CGRect(origin: .zero, size: rectangleSize), cornerRadius: minLength).addClip()
        self.draw(in: CGRect(origin: CGPoint(x: (minLength - self.size.width) / 2, y: (minLength - self.size.height) / 2), size: self.size))

        let newImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return newImage
    }
}

コードを解説すると、UIImageViewを継承したクラスでimageのプロパティをオーバーライドして、画像を設定するタイミングで丸画像を作成して、Viewに当てはめるということをしました。
また layoutSubViews もオーバーライドして、そのタイミングで layer.cornerRadius を設定することでlayer自体も丸に変更しているため、 layer.borderColorlayer.borderWidth を指定して枠線をつけてももうまく動作します。

ライブラリ化してみました

簡単なコードではあるものの、よく使うものではあるし前から興味もあったので初となるライブラリ公開をしてみました。
Cocoapods と Carthage の両方に対応しています。(多分 Swift Package Manger でも動く)
(Carthageを使う場合にはREADMEに記載の注意ポイントがありましたのでご注意ください)

https://github.com/saku/EnhancedCircleImageView

本当は CircleImageView というシンプルな名前にしたかったのですが、既に同名のライブラリがCocoapodsに登録されてしまっていたので、無駄に Enhanced という接頭辞をつけてみましたw
上のGithubのコードにはサンプルコードも含めていますので、是非一度ご覧になっていただけると嬉しいです。
また少しでも参考になったならばGithubのStarなんかもいただけると筆者が泣いて喜びます。

おわりに

今までは clipToBoundlayer.masksToBounds をtrueにして layer.cornerRadius を設定する方法しかつかってなかったのでこの方法を知ったときには驚きました。
が、けっこう簡単なクラスとして作れるので便利でいいですよね。

明日は @tkngue さんの 負荷試験のためのノウハウ と Webフレームワークの負荷試験 です。
みなさん、お楽しみに!


『 Swift 』Article List