UITextFieldやUITextViewで入力時において部分的に文字色を変更する方法

# はじめに
こんにちは:leaves:
**UITextField**や**UITextView**などで以下のように:syringe:**一部分だけ色をつける実装**:syringe:について少し悩んだので共有してみたいと思います。至らぬ点など多々あると思いますが、コメントなど頂けたら幸いです。

![9.gif](https://qiita-image-store.s3.amazonaws.com/0/128250/7598b035-fbd2-b756-7caf-4847984efe0c.gif)

# textColorについて
**UITextField**や**UITextView**には`textColor`というプロパティがあります。
このプロパティに`UIColor`を指定することで文字の色を変更することができますが、以下のように文字の色全てが変わってしまいます。

![6.gif](https://qiita-image-store.s3.amazonaws.com/0/128250/c8ffa2cc-1300-8881-1758-2be4539fe7ff.gif)

そこで今回のような実装の場合は**UITextField**や**UITextView**がプロパティとして持つ**NSAttributedString**を利用して実装します。

# サンプルコード
StoryboardにてUITextFieldとUIButtonを3つ置き、IB接続しています。

```MainViewController.swift
import UIKit

class MainViewController: UIViewController {
    
    @IBOutlet weak var textField: UITextField!
    
    var typingColor = UIColor.black
    
    override func viewDidLoad() {
        super.viewDidLoad()
        textField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
        typingColor = textField.tintColor
    }
    
    @IBAction func blackAction(_ sender: Any) {
        typingColor = UIColor.black
        //キャレットの色を変更
        textField.tintColor = typingColor
        //文字に色を設定
        textField.typingAttributes?.merge([NSForegroundColorAttributeName: typingColor])
        //文字を選択されている状態でボタンを押された時の処理
        if textField.isTextSelected {
            if let range = textField.selectedTextNSRange {
                guard let attributedText = textField.attributedText else { return }
                let mutable = NSMutableAttributedString(attributedString: attributedText)
                mutable.addAttributes([NSForegroundColorAttributeName: typingColor], range: range)
                textField.attributedText = mutable
            }
        }
    }
    
    @IBAction func blueAction(_ sender: Any) {
        typingColor = UIColor(hex: 0x4990E2)
        textField.tintColor = typingColor
        textField.typingAttributes?.merge([NSForegroundColorAttributeName: typingColor])
        
        if textField.isTextSelected {
            if let range = textField.selectedTextNSRange {
                guard let attributedText = textField.attributedText else { return }
                let mutable = NSMutableAttributedString(attributedString: attributedText)
                mutable.addAttributes([NSForegroundColorAttributeName: typingColor], range: range)
                textField.attributedText = mutable
            }
        }
    }
    
    @IBAction func greenAction(_ sender: Any) {
        typingColor = UIColor(hex: 0x7ED321)
        textField.tintColor = typingColor
        textField.typingAttributes?.merge([NSForegroundColorAttributeName: typingColor])
        
        if textField.isTextSelected {
            if let range = textField.selectedTextNSRange {
                guard let attributedText = textField.attributedText else { return }
                let mutable = NSMutableAttributedString(attributedString: attributedText)
                mutable.addAttributes([NSForegroundColorAttributeName: typingColor], range: range)
                textField.attributedText = mutable
            }
        }
    }
    
    //TextFieldの内容に変化があった時に呼ばれます
    //これが無いと、文字を消した時にキャレットが直前の色になってしまいます
    func textFieldDidChange(_ textField: UITextField) {
        textField.typingAttributes?.merge([NSForegroundColorAttributeName: typingColor])
    }
}

extension UITextInput {
    //NSRangeとして取得します
    var selectedTextNSRange: NSRange? {
        guard let range = selectedTextRange else { return nil }
        let location = offset(from: beginningOfDocument, to: range.start)
        let length = offset(from: range.start, to: range.end)
        return NSRange(location: location, length: length)
    }
    //文字が選択されているかどうか
    var isTextSelected: Bool {
        guard let range = selectedTextRange else { return false }
        return !range.isEmpty
    }
}

extension UIColor {
    convenience init(hex: Int, alpha: CGFloat = 1.0) {
        let red = CGFloat((hex & 0xFF0000) >> 16) / 255.0
        let green = CGFloat((hex & 0xFF00) >> 8) / 255.0
        let blue = CGFloat(hex & 0xFF) / 255.0
        let a = alpha
        self.init(red: red, green: green, blue: blue, alpha: a)
    }
}

extension Dictionary {
    mutating func merge<S: Sequence>(_ other: S) where S.Iterator.Element == (key: Key, value: Value) {
        for (key, value) in other {
            self[key] = value
        }
    }
}
```

※若干単調になってしまい、申し訳ございません。(><)

- **UITextField**や**UITextView**が持つ`typingAttributes`というプロパティに対して、文字色やフォントの情報を追加設定しています。

- `selectedTextRange`のプロパティを用いることで、テキストを選択してボタンを押した場合に**選択されている部分の色**を変更できるようにしています。

- `textFieldDidChange`メソッドにてテキスト変更時に`typingAttributes`を設定し、色のついた文字を消した時の直前の色とキャレットの色を合わせています。

# [UITextRange](https://developer.apple.com/reference/uikit/uitextrange)のisEmptyについて
サンプルではExtensionで分かりやすく書き直してみましたが正確には以下のようになっているみたいです。

![U.png](https://qiita-image-store.s3.amazonaws.com/0/128250/285f3d0a-bfe5-8d37-1f26-dbaca1866e57.png)

# さいごに
今回の例では**UITextField**を利用させていただきましたが、**UITextView**での実装もほとんど変わりません。**UITextField**には`textFieldDidChange`が**UITextFieldDelegate**に無かったのでIBで接続するかaddTargetする方法にしました。

# 参考にさせていただいた記事
- [textFieldDidChangeについて](http://www.sumimasen.com/tech/28394933.html)

- [UITextFieldのキャレットを操作（UITextRangeとUITextPosition)　the moon at dawn](http://glayash.blogspot.jp/2012/08/uitextfielduitextrageuitextposition.html)

見て頂いてありがとうございます。