UILabelにイメージを表示して色を変える記事は星の数ほどある。
大体、以下のような感じで実現できる。
- UIImage を.withRenderingMode(.alwaysTemplate) して生成
- NSTextAttachmentのimageにセットする
- NSAttributedString(attachment:) をNSMutableAttributedStringにappendする
- NSMutableAttributedStringにaddAttribute(_,value:range:)してNSAttributedString.Key.foregroundColorをキーとしてUIColorをセットする
こんなの
let image = UIImage(named: "foo")!
let attributedString = NSMutableAttributedString(string:"")
let templateImage = image.withRenderingMode(.alwaysTemplate)
let textAttachment = NSTextAttachment()
textAttachment.image = templateImage
textAttachment.bounds = CGRect(x: 0, y: 0, width: 20, height: 20)
let attrString = NSAttributedString(attachment: textAttachment)
attributedString.append(attrString)
attributedString.addAttribute(
NSAttributedString.Key.foregroundColor,
value: UIColor.green,
range: NSRange(location: 0, length: attributedString.length))
label.attributedText = attributedString
しかし、もう何度も書いてるはずのこのコードに2時間くらいハマった。
- Xcode10.1
- Swift4.2
問題
セットしたUIColorが全く反映されない。
原因
NSMutableAttributedStringの先頭が文字列の場合は問題ないのだが、
なんと先頭がNSTextAttachmentから始まる場合にaddAttributeしてもイメージにUIColorが適用されないということがわかった。
NSMutableAttributedStringの内容が
"😀text"だと問題あり、
"text😀"だと問題ない。
問題あり
問題なし
対策
NSMutableAttributedStringの先頭に文字列(スペース)を入れて回避する。
let image = UIImage(named: "foo")!
let attributedString = NSMutableAttributedString(string:"") // <- ここに入れてもOK
let templateImage = image.withRenderingMode(.alwaysTemplate)
let textAttachment = NSTextAttachment()
textAttachment.image = templateImage
textAttachment.bounds = CGRect(x: 0, y: 0, width: 20, height: 20)
let attrString = NSAttributedString(attachment: textAttachment)
attributedString.append(attrString)
attributedString.insert(NSAttributedString(string: " "), at: 0) // <- これ
attributedString.addAttribute(
NSAttributedString.Key.foregroundColor,
value: UIColor.green,
range: NSRange(location: 0, length: attributedString.length))
label.attributedText = attributedString
これで一件落着、と思いきやこれでもまだ罠がある。
先頭に文字列を入れることで先頭の文字列分後退するし、イメージのみの場合と比べてbaselineが少し上になる。
密にレイアウトしているアプリだと上が切れたり下に変なスペースが生まれたりするし、
大体、 コード的に非常に気持ち悪い。
先頭がイメージ
先頭が文字列
問題は回避されるが、先頭にスペース(文字列)ができるのとbaselineが微妙に上に。
対策の対策
頑張ってbaselineを合わせたとしても先頭にスペースは頂けない。
またAttributedStringの処理に依存してレイアウトするのも後々痛い目を見そう。
ちょっと泥臭いがイメージ自体に色をオーバレイさせる方法にした。
class HogeView {
func fuga() {
let image = UIImage(named: "foo")!
let attributedString = NSMutableAttributedString(string:"")
let templateImage = image.withRenderingMode(.alwaysTemplate)
let textAttachment = NSTextAttachment()
textAttachment.image = templateImage.overray(with: .red)
textAttachment.bounds = CGRect(x: 0, y: 0, width: 20, height: 20)
let attrString = NSAttributedString(attachment: textAttachment)
attributedString.append(attrString)
label.attributedText = attributedString
}
}
extension UIImage {
func overray(with fillColor: UIColor) -> UIImage {
// using scale correctly preserves retina images
UIGraphicsBeginImageContextWithOptions(size, false, scale)
let context: CGContext! = UIGraphicsGetCurrentContext()
assert(context != nil)
// correctly rotate image
context.translateBy(x: 0, y: size.height)
context.scaleBy(x: 1.0, y: -1.0)
let rect = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)
// draw tint color
context.setBlendMode(.normal)
fillColor.setFill()
context.fill(rect)
// mask by alpha values of original image
context.setBlendMode(.destinationIn)
context.draw(self.cgImage!, in: rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
結果
感想
文章の先頭にアイコンとか何らかのイメージ置いたりするケースは結構ありそうなもんだが。。