iOSのハイパフォーマンスなCorner Rounding Strategy

iOSのハイパフォーマンスなCorner Rounding Strategy

この記事はeureka Native Advent Calendar 2017 — Qiitaの3日目の記事です。


こんにちは。Pairs JP事業部でスクラムマスター & iOSエンジニアをしているです。今回は、ハイパフォーマンスな角丸のUIの実現方法について紹介したいと思います。Pairsもそうですが、多くのアプリで写真などを丸く切り取ることがあると思います。書き方によっては、アプリのパフォーマンスに影響を及ぼすので、参考にしていただけると嬉しいです。


今回の記事はTextureCorner Roundingのドキュメントを参考にしています。1日目の記事でも紹介しましたが、Pairs GlobalのアプリはTextureをベースに開発されています。

目次

  • CALayerのcornerRadiusは避けた方が良い
  • ハイパフォーマンスな実装方法3つ
  • 角丸の種類
  • Corner Rounding Strategyフローチャート
  • さいごに

CALayerのcornerRadiusは避けた方が良い

たとえば画像を丸くしたい場合、UIImageViewのlayerのcornerRadiusを設定することが多いと思います。

imageView.layer.cornerRadius = imageView.bounds.size.width / 2

しかし、これはGPUのリソースを多く使ってしまいます。コンテンツをスクロールしたとき、毎フレームで画像の切り抜き処理のためのオフスクリーンレンダリングが走ります。さらに、レイヤーのコンテンツ(ここでは画像)が変わっていない場合や、レイヤーに関係ないViewがアニメーションした場合などでも発生します。


上記の理由から、CALayerのcornerRadiusは、静的な画面でのみ使用することをオススメします。


また、オフスクリーンレンダリングの処理コストはTime Profilerには表示されません。Core AnimationのRender Serverが処理しているためです。

ハイパフォーマンスな実装方法3つ

CALayerのcornerRadiusを避けて、ハイパフォーマンスな実装方法を3つ紹介したいと思います。1からパフォーマンスが高い順番になっています。

  1. 背景が不透明な角丸クリップ画像をつくる
  2. 背景が透明な角丸クリップ画像をつくる
  3. 角丸だけのViewを重ねる

それぞれの実装方法の詳細について説明したいと思います。今回はわかりやすさのために、背景が青い場合のサンプルコードを紹介します。

1. 背景が不透明な角丸クリップ画像をつくる

不透明な背景も含めて1枚の画像をつくります。View Debuggerで見るとこのようになっています。

こちらの画像はUIGraphicsのAPIで実現できます。UIGraphicsBeginImageContextWithOptionsの2つ目の引数は不透明(opaque)かどうかなので、ここではtrueとしています。

extension UIImage {
func rounded(with backgroundColor: UIColor) -> UIImage? {
// opaque = true
// scale = 0.0と指定するとデバイスごとのscaleになる
UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let context = UIGraphicsGetCurrentContext()
let rect = CGRect(origin: .zero, size: size)
// 背景色を塗る
context?.setFillColor(backgroundColor.cgColor)
context?.fill(rect)
// 丸い画像を描画する
let path = UIBezierPath(ovalIn: rect)
path.addClip()
draw(in: rect)
let roundedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return roundedImage
}
}
Backgroundスレッドで画像を作成して、MainスレッドでUIImageViewに画像を渡します。
DispatchQueue.global(qos: .background).async {
guard let image = someImage.rounded(with: .pairsBlue) else {            
return
}
DispatchQueue.main.async {
self.imageView.image = image
}
}
2. 背景が透明な角丸クリップ画像をつくる
少し分かりづらいかもしれませんが、角丸の裏側が透けています。
実装方法は1とほぼ同じですが、UIGraphicsBeginImageContextWithOptionsの2つ目の引数opaqueはfalseにします。
extension UIImage {
func roundedWithTransparentBackground() -> UIImage? {
// opaque = false
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
let rect = CGRect(origin: .zero, size: size)
// 丸い画像を描画する
let path = UIBezierPath(ovalIn: rect)
path.addClip()
draw(in: rect)
let roundedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return roundedImage
}
}
3. 角丸だけのViewを重ねる
画像やUIImageView自体を処理するのではなく、上から角丸だけのViewを重ねます。
角丸のために、MaskCornerViewを作ります。
final class MaskCornerView: UIView {
init(color: UIColor, frame: CGRect) {
super.init(frame: frame)
shapeLayer.fillRule = kCAFillRuleEvenOdd
shapeLayer.fillColor = color.cgColor
let maskPath = UIBezierPath(roundedRect: bounds, cornerRadius: bounds.height / 2)
let path = UIBezierPath(rect: bounds)
path.append(maskPath)
shapeLayer.path = path.cgPath
}
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
private var shapeLayer: CAShapeLayer {
return self.layer as! CAShapeLayer
}
}
あとは、imageViewの上から被せるだけです。
contentView.addSubview(imageView)
let maskCornerView = MaskCornerView(color: .pairsBlue, frame: imageView.frame)
contentView.addSubview(maskCornerView)
角丸の種類
上記で説明した3つの角丸の実装方法は、適用できる角丸の種類が異なります。そのため、まずは角丸の種類を特定する必要があります。切り口としては2軸があります。
1. コンテンツが動的に変わるか?
角丸の裏側と内側が動的に変わる場合が考えられます。
2. 他のViewと被っているか?
角丸が1つのViewだけに関わる場合と、いくつかのViewに被る場合が考えられます。
Corner Rounding Strategyフローチャート
以上の話をまとめると、角丸の種類と最適な実装方法を示す1枚のフローチャートが出来上がります。フローチャートの実装方法の数字はパフォーマンスが高い順番になっており、4番目はCALayerのcornerRadiusになります。
さいごに
現在、Pairs JPのiOSアプリでは角丸だけのViewを被せて角丸を実現しています。今後は、角丸も含めた画像を作成するように改修していきたいと思っています。また、角丸も含めた画像をキャッシュすることで2回目以降のクリップ処理をなくすことができるので、キャッシュの方法についても改善の余地があると思います。
Like what you read? Give eureka_developers a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.