LoginSignup
4
2

More than 3 years have passed since last update.

NSTextAttachmentで追加したイメージの色を変える時にハマった

Last updated at Posted at 2019-08-23

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😀"だと問題ない。

問題あり

failure.png

問題なし

success.png

対策

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が少し上になる。
密にレイアウトしているアプリだと上が切れたり下に変なスペースが生まれたりするし、
大体、 コード的に非常に気持ち悪い。

先頭がイメージ

failure.png

先頭が文字列

問題は回避されるが、先頭にスペース(文字列)ができるのとbaselineが微妙に上に。
success.png

対策の対策

頑張って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!
     }
 }

結果

solved.png

感想

文章の先頭にアイコンとか何らかのイメージ置いたりするケースは結構ありそうなもんだが。。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2