はじめに
通常だとUIImageView
に画像を設定してUIImageView
自体に角丸をつけたりすることが多いと思うのですが、NSTextAttachment
でUITextView
上などに画像を簡単に置きたい場合にはUIImage
で置くしかありません。そのため、UIImage
の画像自体に角丸をつける必要があったので、ついでに描画のパフォーマンスを調べようと思います。
描画方法
その1
-
UIImage
からCGImage
を取り出してリサイズ - リサイズされた画像を
UIImageView
設定 - マスク用の
UIBezierPath
も設定 -
UIImageView
のCALayer
をCGContext
上に描画して、そこから新しいUIImage
を生成
private struct SizeReletaion {
private enum Element {
case Height
case Width
}
let longerSide: (value: CGFloat, element: Element)
let shoterSide: (value: CGFloat, element: Element)
init(size: CGSize, scale: CGFloat) {
longerSide = size.width > size.height ? (size.width * scale, .Width) : (size.height * scale, .Height)
shoterSide = size.width > size.height ? (size.height * scale, .Height) : (size.width * scale, .Width)
}
func centerTrimRect() -> CGRect {
let delta = (longerSide.value - shoterSide.value) / 2
let x = longerSide.element == .Height ? 0 : delta
let y = longerSide.element == .Width ? 0 : delta
return CGRect(x: x, y: y, width: shoterSide.value, height: shoterSide.value)
}
func relativeCenterTrimRect(targetSize: CGSize) -> CGRect {
let delta = -(longerSide.value - shoterSide.value) / 2
let x = longerSide.element == .Height ? 0 : delta
let y = longerSide.element == .Width ? 0 : delta
let width = longerSide.element == .Width ? longerSide.value : shoterSide.value
let height = longerSide.element == .Height ? longerSide.value : shoterSide.value
let scale = shoterSide.element == .Height ? targetSize.height / height : targetSize.width / width
return CGRect(
x: x * scale,
y: y * scale,
width: width * scale,
height: height * scale
)
}
}
extension UIImage {
func roundedCenterTrimImage1(cornerRadius: CGFloat, targetSize: CGSize) -> UIImage? {
let sizeRelation = SizeReletaion(size: size, scale: scale)
guard let newImage = trimmedImage(sizeRelation.centerTrimRect()) else {
return nil
}
let scaledSize = CGSize(width: targetSize.width * scale, height: targetSize.height * scale)
UIGraphicsBeginImageContext(scaledSize)
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: scaledSize.width, height: scaledSize.height), cornerRadius: cornerRadius)
let imageView = UIImageView(image: newImage)
imageView.frame = CGRect(x: 0, y: 0, width: scaledSize.width, height: scaledSize.height)
imageView.layer.masksToBounds = true
let maskLayer = CAShapeLayer()
maskLayer.path = path.CGPath
imageView.layer.mask = maskLayer
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
imageView.layer.renderInContext(context)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
func trimmedImage(rect: CGRect) -> UIImage? {
guard let cgImage = CGImageCreateWithImageInRect(self.CGImage, rect) else {
return nil
}
return UIImage(CGImage: cgImage)
}
}

その2
- フルサイズの画像を
UIImageView
に設定 - マスク用の
UIBezierPath
も設定 -
UIImageView
のCALayer
をCGContext
上に描画して、そこから新しいUIImage
を生成
UIImageViewとCGContext
extension UIImage {
func roundedCenterTrimImage2(cornerRadius: CGFloat, targetSize: CGSize) -> UIImage? {
let scaledSize = CGSize(width: targetSize.width * scale, height: targetSize.height * scale)
UIGraphicsBeginImageContext(scaledSize)
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: scaledSize.width, height: scaledSize.height), cornerRadius: cornerRadius)
let imageView = UIImageView(image: self)
imageView.frame = CGRect(x: 0, y: 0, width: scaledSize.width, height: scaledSize.height)
imageView.contentMode = .ScaleAspectFill
imageView.layer.masksToBounds = true
let maskLayer = CAShapeLayer()
maskLayer.path = path.CGPath
imageView.layer.mask = maskLayer
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
imageView.layer.renderInContext(context)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}

その3
-
UIImage
とマスク用のUIBezierPath
をCGContext
に描画して、そこから新しいUIImage
として生成
CGContext
extension UIImage {
func roundedCenterTrimImage3(cornerRadius: CGFloat, targetSize: CGSize) -> UIImage? {
let sizeRelation = SizeReletaion(size: size, scale: scale)
let scaledSize = CGSize(width: targetSize.width * scale, height: targetSize.height * scale)
UIGraphicsBeginImageContext(scaledSize)
drawInRect(sizeRelation.relativeCenterTrimRect(scaledSize))
let context = UIGraphicsGetCurrentContext()
let path = roundedMaskPath(cornerRadius, targetSize: scaledSize)
CGContextAddPath(context, path.CGPath)
CGContextClip(context)
CGContextClearRect(context, CGRect(x: 0, y: 0, width: scaledSize.width, height: scaledSize.height))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
private func roundedMaskPath(cornerRadius: CGFloat, targetSize: CGSize) -> UIBezierPath {
struct CornerContainer {
let corner: UIRectCorner, point: CGPoint
func linePoint(cornerRadius: CGFloat) -> CGPoint {
let operatorValue: CGFloat
switch corner {
case UIRectCorner.TopLeft, UIRectCorner.BottomLeft: operatorValue = 1
case UIRectCorner.TopRight, UIRectCorner.BottomRight: operatorValue = -1
default: return .zero
}
return CGPoint(x: point.x + (operatorValue * (cornerRadius)), y: point.y)
}
func curvePoint(cornerRadius: CGFloat) -> CGPoint {
let operatorValue: CGFloat
switch corner {
case UIRectCorner.TopLeft, UIRectCorner.TopRight: operatorValue = 1
case UIRectCorner.BottomRight, UIRectCorner.BottomLeft: operatorValue = -1
default: return .zero
}
return CGPoint(x: point.x, y: point.y + (operatorValue * (cornerRadius )))
}
}
let corners: [CornerContainer] = [
CornerContainer(corner: .TopLeft, point: .zero),
CornerContainer(corner: .TopRight, point: CGPoint(x: targetSize.width, y: 0)),
CornerContainer(corner: .BottomRight, point: CGPoint(x: targetSize.width, y: targetSize.height)),
CornerContainer(corner: .BottomLeft, point: CGPoint(x: 0, y: targetSize.height))
]
let path = UIBezierPath()
corners.forEach {
let cornerRoundPath = UIBezierPath()
cornerRoundPath.moveToPoint($0.point)
cornerRoundPath.addLineToPoint($0.linePoint(cornerRadius))
cornerRoundPath.addQuadCurveToPoint($0.curvePoint(cornerRadius), controlPoint: $0.point)
cornerRoundPath.addLineToPoint($0.point)
cornerRoundPath.closePath()
path.appendPath(cornerRoundPath)
}
return path
}
}

ちなみに計測の方法は以下のように
UIImage
生成前のNSDate
と生成後のNSDate
の差になってます。
ViewController.swift
class ViewController: UIViewController {
@IBOutlet var imageViews: [UIImageView]!
@IBOutlet weak var baseImageView: UIImageView!
@IBOutlet weak var textView: UITextView!
@IBAction func didTapRender1(sender: UIButton) {
let image = printRenderingTime(0) {
return $0?.roundedCenterTrimImage1(8, targetSize: CGSize(width: 80, height: 80))
}
imageViews[0].image = image
}
@IBAction func didTapRender2(sender: UIButton) {
let image = printRenderingTime(1) {
return $0?.roundedCenterTrimImage2(8, targetSize: CGSize(width: 80, height: 80))
}
imageViews[1].image = image
}
@IBAction func didTapRender3(sender: UIButton) {
let image = printRenderingTime(2) {
return $0?.roundedCenterTrimImage3(8, targetSize: CGSize(width: 80, height: 80))
}
imageViews[2].image = image
}
private func printRenderingTime(index: Int, rendring: (UIImage?) -> UIImage?) -> UIImage? {
let image = baseImageView.image
let startDate = NSDate()
let roundedImage = rendring(image)
let finishDate = NSDate()
let rendringTime = finishDate.timeIntervalSince1970 - startDate.timeIntervalSince1970
let text = "Rendering\((index + 1)) Time: \(rendringTime)"
textView.insertText(text)
textView.insertText("\n")
print(text)
return roundedImage
}
}
最後に
CGImage
でリサイズしたものをUIImageView
でマスクをかけたものを描画をして、UIImage
にしたものが一番速かったです。
CGContext
で生成したものが一番速いのかと思ってたのですが、意外な結果でした。