すっさんぽ
  1. Posts/

プレースホルダ付きUITextViewの作り方

·

どうしてUITextViewはプレースホルダを持っていないのでしょうか。 どうしてUITextFieldは複数行にならないのでしょうか。 そんな気持ちにこれまでも幾度となく遭遇してきました。

今回は、UITextViewにプレースホルダを実装します。 ただし、UILabelのaddSubviewはせず、CATextLayerを追加する方法でやってみます。

なぜUILabelを使わないのか #

ViewControllerのviewの階層を邪魔したくなかったからです。 CALayerを使えば、カスタムUIをViewのなかに閉じ込めることができます。

※ もちろん、addSubviewする方式がやりやすいです。
※ CALayerを使うほうが、都合がいいこともあります。

動作スクショ #

こんな動きになります。

実装 #

通常のUITextViewとはちょっと違う使い方になります。 TextViewのfontパラメータの変更を、CATextLayerに伝播させることができなかったので、setFont(_:)というメソッドを生やしています。もしかしてKVOでできたりしたのかな……?

import UIKit

class PlaceholderTextView: UITextView {

    override var text: String! {
        didSet {
            super.text = text
            changePlaceholderHidden()
        }
    }

    var placeholderText: String? {
        didSet {
            textLayer.string = placeholderText
        }
    }

    var placeholderColor: UIColor? = .gray {
        didSet {
            textLayer.foregroundColor = placeholderColor?.cgColor
        }
    }

    func setFont(_ font: UIFont?) {
        self.font = font
        prepareStyle()
    }

    private var textLayer: CATextLayer!

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        prepare()
    }

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        prepare()
    }

    private func prepare() {
        // textLayerとピッタリ揃えたいので、
        // textContainerInset と lineFragmentPadding をゼロにする
        textContainerInset = .zero
        textContainer.lineFragmentPadding = 0

        textLayer = CATextLayer()
        textLayer.string = placeholderText
        textLayer.foregroundColor = placeholderColor?.cgColor
        textLayer.contentsScale = UIScreen.main.scale
        textLayer.isWrapped = true

        layer.addSublayer(textLayer)

        prepareStyle()

        changePlaceholderHidden()

        NotificationCenter.default.addObserver(self,
                                               selector: #selector(textDidChange(_:)),
                                               name: UITextView.textDidChangeNotification,
                                               object: nil)
    }

    private func prepareStyle() {
        if let font = font {
            textLayer.font = CGFont(font.fontName as CFString)
            textLayer.fontSize = font.pointSize
        }
    }

    override func layoutSublayers(of layer: CALayer) {
        // CATextLayerを、UITextViewのtextContainerInsetにピッタリさせる
        // (あれ、でも prepare() で inset は zero にしているので、無意味かもしれない……)
        let width = layer.frame.size.width
            - textContainerInset.left - textContainerInset.right
        let height = layer.frame.size.height
            - textContainerInset.top - textContainerInset.bottom
        textLayer.frame = CGRect(origin: .init(x: textContainerInset.left
                                                + alignmentRectInsets.left,
                                                y: textContainerInset.bottom),
                                  size: .init(width: width, height: height))
    }

    @objc
    private func textDidChange(_ notification: Notification?) {
        changePlaceholderHidden()
    }

    private func changePlaceholderHidden() {
        textLayer.isHidden = !text.isEmpty
    }
}


/// in a ViewController...

private func setupTextView() {
    placeholderTextView.placeholderText = "プレースホルダ"
    placeholderTextView.setFont(.boldSystemFont(ofSize: 15))     // ここが通常と異なる

    // その他の調整は、おこのみで……
    placeholderTextView.contentInset = .init(top: 20, left: 0, bottom: 8, right: 0)
    placeholderTextView.delegate = self
    placeholderTextView.returnKeyType = .done
}

注意点 #

  • StoryboardのデザインでTextViewに文字が入力されていると、初期表示でプレースホルダが表示されないことがある
    • user defined runtime attributesで消してあげればうまくいくかも(試していない)

まとめ #

CATextLayerを使って、UITextViewにプレースホルダを実装する方法を紹介しました。 UILabelを使う方法が一般的と思いますが、viewの階層を邪魔せずにプレースホルダを追加する場合は、CATextLayerを使うのがよいかなと思います。 CATextLayerの仕様を十分理解できているわけではないので、フォントの設定がいまいちなところは課題です。また良い方法を見つけたら、改訂しておこうと思います。


(参考にしたブログ)

失念……。
ただし、CATextLayerをつかってプレースホルダを作るブログは見当たりませんでした。