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

UILabelにイメージを表示して色を変える記事は星の数ほどある。

大体、以下のような感じで実現できる。
* UIImage を.withRenderingMode(.alwaysTemplate) して生成
* NSTextAttachmentのimageにセットする
* NSAttributedString(attachment:) をNSMutableAttributedStringにappendする
* NSMutableAttributedStringにaddAttribute(_,value:range:)してNSAttributedString.Key.foregroundColorをキーとしてUIColorをセットする

こんなの

```Swift
        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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/01654ad7-465f-4287-50e3-17983fec1e1b.png)

### 問題なし
![success.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/48a41dce-e4b9-3e06-55c7-780871093e8e.png)




# 対策

NSMutableAttributedStringの先頭に文字列（スペース）を入れて回避する。

```Swift
        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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/161421a8-322e-14db-3910-386a803c89a5.png)

### 先頭が文字列
問題は回避されるが、先頭にスペース（文字列）ができるのとbaselineが微妙に上に。
![success.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/a0abd450-f89a-02c7-47a5-6afc96f43cb6.png)

# 対策の対策
頑張ってbaselineを合わせたとしても先頭にスペースは頂けない。
またAttributedStringの処理に依存してレイアウトするのも後々痛い目を見そう。
ちょっと泥臭いがイメージ自体に色をオーバレイさせる方法にした。

```Swift
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/8245149d-bce5-7b30-5279-254da91a3aa1.png)

# 感想

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