プレースホルダ付きUITextViewの作り方
Table of Contents
どうして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をつかってプレースホルダを作るブログは見当たりませんでした。