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

  • 7
    いいね
  • 0
    コメント

はじめに

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

9.gif

textColorについて

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

6.gif

そこで今回のような実装の場合はUITextFieldUITextViewがプロパティとして持つ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
        }
    }
}

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

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

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

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

UITextRangeのisEmptyについて

サンプルではExtensionで分かりやすく書き直してみましたが正確には以下のようになっているみたいです。

U.png

さいごに

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

参考にさせていただいた記事

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