✏️

iOS | Placeholder つきの TextView を作る

2022/11/23に公開

作りたいもの

何も入力されていない場合は Placeholder が表示され、何かしら入力されると Placeholder が消える TextView を作ります。

Android だと EditText に hint を設定すれば一瞬で実装できるのですが、iOS は自分で実装する必要があるらしく。。どうにかなりませんか Apple さん。。

実装方法

  1. TextView を継承した HintTextView クラスを作る
  2. TextView の中に Label を置く
  3. TextView に文字が入力されているかどうかで Label の表示/非表示を切り替える
import UIKit

// 1. TextView を継承した HintTextView クラスを作る
@IBDesignable
class HintTextView: UITextView {
    
    // Placeholder として表示するテキスト
    var hintText = "" {
        didSet {
            hintLabel.text = hintText
        }
    }
    
    private lazy var hintLabel: UILabel = {
        let label = UILabel()
        label.lineBreakMode = .byWordWrapping
        label.backgroundColor = .clear
        label.numberOfLines = 0
        label.textColor = .systemGray
        return label
    }()
    
    // 追記: テキストの初期値を入れるときは textViewDidChange が発火しないのでこちらのメソッドを使用する
    // (もっと良い方法がありそうですが...)
    func setText(_ text: String) {
        self.text = text
        changeVisibility()
    }
    
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        configure()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        configure()
    }
    
    private func configure() {
        hintLabel.translatesAutoresizingMaskIntoConstraints = false
	
	// 2. TextView の中に Label を置く
        addSubview(hintLabel)
        
	// Placeholder の表示位置を調整
        NSLayoutConstraint.activate([
	    // hintLabe とその親である HintTextView のトップの余白
            hintLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 5),
	    // hintLabe とその親である HintTextView で X 軸を一致させる
            hintLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor),
	    // hintLabe はその親である HintTextView よりも幅を 10 小さくする(左右に 5 ずつ余白を設ける)
            hintLabel.widthAnchor.constraint(equalTo: self.widthAnchor, constant: -10),
        ])
        
        self.delegate = self
    }
    
    private func changeVisibility() {
        if self.text.isEmpty {
            hintLabel.isHidden = false
        } else {
            hintLabel.isHidden = true
        }
    }
}

extension HintTextView: UITextViewDelegate {
    func textViewDidChange(_ textView: UITextView) {
        // 3. TextView に文字が入力されているかどうかで Label の表示/非表示を切り替える
        changeVisibility()
    }
}

個人的に hintLabel の位置の調整でつまずきました。
制約を何もつけない場合、TextView のカーソルの位置と Placeholder の位置にズレが生じて違和感のある表示になるので、位置を調整する必要があります。また、Placeholder が複数行になっても対応できる必要があります。

最初は hintLabel と親(HintTextView)の間に 上、左、右それぞれ 5 ずつ余白を空ける制約をつけていたのですが、これだと Placeholder が長い場合に改行してくれませんでした。

テキストが長いと親 View を突き抜けて右に伸び続けてしまうようです。

最終的には X 軸を親 View と一致させ、幅を親 View より 10 小さくすることで意図どおりの挙動になってくれました。

使い方

TextView を適当な箇所に置きます。今回は画面全体が TextView になるように制約をつけています。

設置した TextView の Custom Class に HintTextView を指定します。

HintTextView と ViewController を @IBOutlet で繋ぎ込み、HintTextView.hintText に任意のテキストを入力します。

hintTextView.hintText = "テキストを入力してください"

これで冒頭の GIF 画像のような TextView が表示できたと思います。

Discussion