【iOS】Place Holder(プレースホルダー)付きのUITextViewを作成する

はじめに

複数行のユーザーインプットを受け付けたいとき、UITextViewをよく使用します。 ユーザーがどのような内容を入力すれば良いかのヒントを与えるためにプレースホルダーの使用を検討することもよくあることだと思います。

ところが、UITextViewにはプレースホルダーが存在しません。したがって、UITextFieldのように placeholder にStringを入れればよい、というように簡単にはいきません。 本記事ではプレースホルダー付きのUITextViewの作成方法を記載します。

環境設定

以下の環境を使用しています。

  • Xcode10.2.1
  • Swift 5.0.1

方法

どういう風に実現するのが良いかと考えましたが、最終的にはUITextViewの上にUILabelを置くことにしました。 以下、作成したカスタムクラスのコードを記載します。

概要としては、UILabelをtextContainerのinsetやlineFragmentPaddingを考慮しながら配置、 編集開始と編集終了のイベントをKVOで受け取り、必要に応じてプレースホルダーの表示・非表示を制御しています。 プレースホルダーの文字列はプレースホルダープロパティに設定するのみで表示されるように作成しています。

import Foundation
import UIKit

@IBDesignable
public final class PlaceHolderTextView: UITextView {
    
    // MARK: - IBInspectable
    
    @IBInspectable
    public var cornerRadius: CGFloat {
        set { layer.cornerRadius = newValue }
        get { return layer.cornerRadius }
    }
    
    @IBInspectable
    public var borderColor: UIColor? {
        set { layer.borderColor = newValue?.cgColor }
        get {
            if let cgColor = layer.borderColor {
                return UIColor(cgColor: cgColor)
            } else {
                return nil
            }
        }
    }
    
    @IBInspectable
    public var borderWidth: CGFloat {
        set { layer.borderWidth = newValue }
        get { return layer.borderWidth }
    }
    
    /// プレースホルダー表示用の文字列
    /// 本プロパティの値がプレースホルダーとして表示される
    @IBInspectable
    public var placeHolder: String? {
        didSet {
            placeHolderLabel.text = placeHolder
        }
    }
    
    /// 本来持っているtextプロパティを上書きして、didSetをつけている
    /// このようにすることで、UITextViewの初期値に
    /// textが入っていた際にプレースホルダーを非表示にできる
    public override var text: String! {
        didSet {
            placeHolderLabel.isHidden = !text.isEmpty
            placeHolderLabel.sizeToFit()
        }
    }
    
    private let placeHolderLabel = UILabel(frame: .zero)
    
    // MARK: - Life Cycle
    
    public override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        commonInit()
    }
        
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    
    override public func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        commonInit()
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self, name: UITextView.textDidBeginEditingNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: UITextView.textDidEndEditingNotification, object: nil)
    }
}

private extension PlaceHolderTextView {
    
    private func commonInit() {
        
        // PlaceHolder表示用のラベルをUITextViewに加えConstraintをつける
        layer.masksToBounds = true
        addSubview(placeHolderLabel)
        
        // TextViewのtextと位置が同じになるように、
        // textContainerInsetとlineFragmentPaddingを考慮してConstraintをかける
        let padding = textContainer.lineFragmentPadding
        placeHolderLabel.translatesAutoresizingMaskIntoConstraints = false

        placeHolderLabel.topAnchor.constraint(equalTo: topAnchor
            , constant: textContainerInset.top).isActive = true
        placeHolderLabel.leadingAnchor.constraint(equalTo: leadingAnchor
            , constant: textContainerInset.left + padding).isActive = true
        placeHolderLabel.bottomAnchor.constraint(equalTo:  bottomAnchor
            , constant: textContainerInset.bottom).isActive = true
        placeHolderLabel.trailingAnchor.constraint(equalTo: trailingAnchor
            , constant: textContainerInset.right + padding).isActive = true
        
        let widthConstant = (textContainerInset.left + textContainerInset.right + padding * 2)
        placeHolderLabel.widthAnchor.constraint(equalTo: widthAnchor
            , constant: -widthConstant).isActive = true
        
        // Fontカラーはgrayにしていますが、必要に応じて変更してください
        placeHolderLabel.font = font
        placeHolderLabel.textColor = UIColor.gray
        placeHolderLabel.numberOfLines = 0
        placeHolderLabel.text = placeHolder

        // Observations
        // UITextViewの編集イベントを受け取るためにKVOの設定をしています
        // UITextViewの編集を始めるとプレースホルダーが消え、
        // 編集完了すると、textの中身をみてプレースホルダーを表示するか判断します
        NotificationCenter.default.addObserver(self, selector: #selector(hidePlaceHolder), name: UITextView.textDidBeginEditingNotification, object: nil)
        
        NotificationCenter.default.addObserver(self, selector: #selector(changePlaceholderVisibility), name: UITextView.textDidEndEditingNotification, object: nil)
    }
    
    @objc func hidePlaceHolder() {
        placeHolderLabel.isHidden = true
    }
    
    @objc func changePlaceholderVisibility() {
        placeHolderLabel.isHidden = !text.isEmpty
    }
}

以上で下記のようなプレースホルダー付きのUITextViewが簡単に使用できます。

f:id:Iganin:20190622220151p:plain
PlaceHolderTextView(プレースホルダー表示)
f:id:Iganin:20190622220219p:plain
PlaceHolderTextView(プレースホルダー非表示)

まとめ

プレースホルダー付きのUITextViewの作成方法をご紹介しました。

要件として上がってきた際にはぜひ使用してみてください。

参考