xyk blog

最近は iOS 開発の記事が多めです。

UIImage の画像を単色で塗りつぶす

検証環境:
Xcode 12.2
Swift 5.3.1

UIImage の画像を単色で塗りつぶす方法について。

元画像

f:id:xyk:20201120194710p:plain

変更後

f:id:xyk:20201121013317p:plain

UIImageView と一緒に使って変更する

UIImage を UIImageView にセットして一緒に使う場合は、UIImage のRenderingMode.alwaysTemplateの指定とUIImageView のtintColorの指定すれば実現できる。

let image = UIImage(named: "foo.png")!
imageView.image = image.withRenderingMode(.alwaysTemplate)
imageView.tintColor = .gray

UIImage 単体で変更する

UIImage 単体で変更したい場合は、ImageContext に元画像と単色画像をブレンドモードを使用して重ね合わせることで実現できる。
(例えばCALayer.contentsに直接セットするUIImage画像を単色にしたいケースなど)
以下2つどちらのやり方でも同様に変更できる。

  • CGBlendMode.destinationInを使う
func tintedImage(source: UIImage, color: UIColor) -> UIImage {
    UIGraphicsBeginImageContextWithOptions(source.size, false, source.scale)
    let rect = CGRect(origin: .zero, size: source.size)

    color.setFill()
    UIRectFill(rect) // 単色の背景画像として描画
    source.draw(in: rect, blendMode: .destinationIn, alpha: 1) // 元画像を前景画像として描画
    
    let resultImage = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()
    return resultImage
}
  • CGBlendMode.destinationInを使う - UIGraphicsImageRenderer版 (iOS10以降)
func tintedImage(source: UIImage, color: UIColor) -> UIImage {
    return UIGraphicsImageRenderer(size: source.size).image { context in
        let rect = CGRect(origin: .zero, size: source.size)
        color.setFill()
        context.fill(rect) // 単色の背景画像として描画
        source.draw(in: rect, blendMode: .destinationIn, alpha: 1) // 元画像を前景画像として描画
    }
}
  • CGBlendMode.sourceInを使う
func tintedImage(source: UIImage, color: UIColor) -> UIImage {
    UIGraphicsBeginImageContextWithOptions(source.size, false, source.scale)
    let context = UIGraphicsGetCurrentContext()!
    let rect = CGRect(origin: .zero, size: source.size)
    
    source.draw(in: rect) // 元画像を背景画像として描画
    color.setFill()
    context.setBlendMode(.sourceIn)
    context.fill(rect)  // 単色の前景画像として描画
    
    let resultImage = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()
    return resultImage
}
  • CGBlendMode.sourceInを使う - UIGraphicsImageRenderer版 (iOS10以降)
func tintedImage(source: UIImage, color: UIColor) -> UIImage {
    return UIGraphicsImageRenderer(size: source.size).image { context in
        let rect = CGRect(origin: .zero, size: source.size)
        source.draw(in: rect) // 元画像を背景画像として描画
        color.setFill()
        context.fill(rect, blendMode: .sourceIn)  // 単色の前景画像として描画
    }
}
  • 追記: ImageContext 内でRenderingMode.alwaysTemplateを使う。

コードが一番短く書ける。

extension UIImage {
    
    func tinted(with color: UIColor) -> UIImage {
        return UIGraphicsImageRenderer(size: size).image { _ in
            color.set()
            withRenderingMode(.alwaysTemplate).draw(at: .zero)
        }
    }
}

ブレンドモードについて

CGBlendModeAppleドキュメントはこちら
上の例で使った2つのブレンドモードの方程式は

  • CGBlendMode.destinationInR = D*Sa
  • CGBlendMode.sourceInR = S*Da

とのこと。
Rは Result、Sは Source、DDestinationaがアルファ値。

  • CGBlendMode.destinationIn はDとSの重なっている部分だけ表示、色はS
  • CGBlendMode.sourceInはSとDの重なっている部分だけ表示、色はD

になる。

以下のブレンド結果の画像例がわかりやすい。
元画像(赤の円)と混ぜ合わせる画像(青の正方形)をブレンドした例。

f:id:xyk:20201121004849p:plain

こちらの画像を引用。