iosswiftuitextviewplaceholder

How to add a placeholder text to UITextView, but, in all cases (even with changing OS) exactly match the text positioning?


As the title says - it's trivial to add a placeholder to UITextView.

Simply use .textDidChangeNotification and add a CATextLayer or even just a UILabel.

The problem is:

absolutely, exactly, definitively finding the identical position so that the placeholder is always precisely where the main text is.

(Thus, if your placeholder text is "Add your comment" you should be able to type "Add your comment" and it's absolutely identical.)

Of course, you can trivially set the text font/etc the same, but how to absolutely match the positioning, even as Apple inevitably slightly moves things around inside UITextView, and depending on what it is inside, it's own size etc etc.

How to?


Solution

  • The only way I know to do it:

    import UIKit
    
    class PlaceholderTextView: UIITextView {
        
        var placeholderText: String = "" {
            didSet { placeholderLabel.text = placeholderText }
        }
        
        override func common() {
            super.common()
            clipsToBounds = true
            layer.cornerRadius = 5.0
            NotificationCenter.default.addObserver(
                self, selector: #selector(tickle),
                name: UITextView.textDidChangeNotification,
                object:nil)
        }
        
        @objc func tickle() {
            placeholderLabel.isHidden = !text.isEmpty
        }
        
    

    and then surprisingly ...

        lazy private var placeholderLabel: UIITextView = {
            // seems strange, but the ONLY way to truly match the position
            // at all times and in all ways, is use another text view
            let p = UIITextView()
            // !NOTE! use the same base as PlaceholderTextView does,
            // example, YourClientSpecificTextView, etc.
            p.isEditable = false
            p.isUserInteractionEnabled = false
            p.backgroundColor = .clear
            p.font = font
            p.textColor = .placeholderText // NOTE
            addSubview(p)
            return p
        }()
        
    

    and then, it's trivial to set the position of the ghosty text view:

    (Don't forget to copy the textContainerInset; the textContainerInset is almost always changed in apps.)

        override func layoutSubviews() {
            super.layoutSubviews()
            placeholderLabel.textContainerInset = textContainerInset
            placeholderLabel.frame = bounds
            
            // conceivably, the font may have been changed dynamically:
            placeholderLabel.font = font
            placeholderLabel.textColor = .placeholderText // NOTE
            
            tickle()
        }
    }
    

    How to unit test ...

        @objc func tickle() {
            placeholderLabel.isHidden = !text.isEmpty
        }
    

    change to

        @objc func tickle() {
            placeholderLabel.isHidden = false //!text.isEmpty
        }
    

    then perform this test:

    Thus, if your placeholder text is "Add your comment" you should be able to type "Add your comment" and it's absolutely identical.

    Example of exact match:

    enter image description here

    Footnote

    What is the common() initializer in the example class? In projects, I just add trivial initializing views for the various iOS views to save typing. (Notice the extra "I" for initializing.)

    So:

    import UIKit
    class UIITextView: UITextView {
        override init(frame: CGRect, textContainer: NSTextContainer?) {
            super.init(frame: frame, textContainer: textContainer)
            common()
        }
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            common()
        }
        func common() {}
    }
    

    Example,

    enter image description here