Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
The correct way to implement a placeholder in a UITextView (Swift)
//
// UITextViewPlaceholder.swift
// TextViewPlaceholder
//
// Copyright (c) 2017 Tijme Gommers <tijme@finnwea.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import UIKit
/// Extend UITextView and implemented UITextViewDelegate to listen for changes
extension UITextView: UITextViewDelegate {
/// Resize the placeholder when the UITextView bounds change
override open var bounds: CGRect {
didSet {
self.resizePlaceholder()
}
}
/// The UITextView placeholder text
public var placeholder: String? {
get {
var placeholderText: String?
if let placeholderLabel = self.viewWithTag(100) as? UILabel {
placeholderText = placeholderLabel.text
}
return placeholderText
}
set {
if let placeholderLabel = self.viewWithTag(100) as! UILabel? {
placeholderLabel.text = newValue
placeholderLabel.sizeToFit()
} else {
self.addPlaceholder(newValue!)
}
}
}
/// When the UITextView did change, show or hide the label based on if the UITextView is empty or not
///
/// - Parameter textView: The UITextView that got updated
public func textViewDidChange(_ textView: UITextView) {
if let placeholderLabel = self.viewWithTag(100) as? UILabel {
placeholderLabel.isHidden = self.text.characters.count > 0
}
}
/// Resize the placeholder UILabel to make sure it's in the same position as the UITextView text
private func resizePlaceholder() {
if let placeholderLabel = self.viewWithTag(100) as! UILabel? {
let labelX = self.textContainer.lineFragmentPadding
let labelY = self.textContainerInset.top - 2
let labelWidth = self.frame.width - (labelX * 2)
let labelHeight = placeholderLabel.frame.height
placeholderLabel.frame = CGRect(x: labelX, y: labelY, width: labelWidth, height: labelHeight)
}
}
/// Adds a placeholder UILabel to this UITextView
private func addPlaceholder(_ placeholderText: String) {
let placeholderLabel = UILabel()
placeholderLabel.text = placeholderText
placeholderLabel.sizeToFit()
placeholderLabel.font = self.font
placeholderLabel.textColor = UIColor.lightGray
placeholderLabel.tag = 100
placeholderLabel.isHidden = self.text.characters.count > 0
self.addSubview(placeholderLabel)
self.resizePlaceholder()
self.delegate = self
}
}
@nithingwl

This comment has been minimized.

Copy link

nithingwl commented Mar 13, 2017

Hi,
Its a great way to extend the textview to add functionality instead of subclassing.
I found a small issue with it--The call to resizePlaceholder() method(Line 76) should be made after adding the label as a subview because, the if let condition in the resizePlaceholder method fails as the label is not a subview yet and the label's frame isn't set.

@tijme

This comment has been minimized.

Copy link
Owner Author

tijme commented Mar 29, 2017

@nithingwl Hi, sorry for the late response, somehow I didn't get a notification.

I fixed the issue by switching those lines. Thanks!

@germs5

This comment has been minimized.

Copy link

germs5 commented Apr 6, 2017

It doesn't seem to handle multiple lines. I am seeing truncation with ellipsis at the end of the first line. Perhaps because textContainer frame is not the same as the UITextView frame?

@tijme

This comment has been minimized.

Copy link
Owner Author

tijme commented Apr 22, 2017

@germs5 Hmm, I didn't implement that indeed. I think there are two things you can try. Either use adjustsFontSizeToFitWidth on the placeholder label or increase the height and make it multiline.

@runningdemo

This comment has been minimized.

Copy link

runningdemo commented May 24, 2017

I think it's better not to take control of the delegate property in the extension. Because developer may need set delegate to another Object and do more stuff. We can listen to the text change this way:

 NotificationCenter.default.addObserver(self,
                                               selector: #selector(textViewDidChange),
                                               name: NSNotification.Name.UITextViewTextDidChange,
                                               object: nil)
@tijme

This comment has been minimized.

Copy link
Owner Author

tijme commented May 24, 2017

@liaa Great solution! I am not able to test this right now but I'll merge it as soon as I have time.

@congnd

This comment has been minimized.

Copy link

congnd commented Jun 5, 2017

what is the license of this lib?

@tijme

This comment has been minimized.

Copy link
Owner Author

tijme commented Jun 5, 2017

@congnd MIT. I just added the license at the top of the Swift file.

@astrokin

This comment has been minimized.

Copy link

astrokin commented Jul 16, 2017

what if i need delegate somewhere else?

@milsawicki

This comment has been minimized.

Copy link

milsawicki commented Jul 24, 2017

@astrokin
@liaa 's answear with adding observer works for me

@seubseub

This comment has been minimized.

Copy link

seubseub commented Aug 18, 2017

@liaa Thanks! :) I used this nice extension. but, when I use textview's delegate, then delegate not work well.. it tangled. but I understand your think, and I implement that, then It work perfectly. thanks!

@tijme

This comment has been minimized.

Copy link
Owner Author

tijme commented Aug 18, 2017

@seubseub You can also use @liaa 's solution 😄

@yuvalt

This comment has been minimized.

Copy link

yuvalt commented Sep 5, 2017

Thanks! I forked the gist and added a few more values to control (font, color, inset) with support for Interface Builder.

@tijme

This comment has been minimized.

Copy link
Owner Author

tijme commented Sep 5, 2017

@yuvalt Ah I see, very nice!

@manolosavi

This comment has been minimized.

Copy link

manolosavi commented Nov 8, 2017

Very helpful! Thanks!
Btw there's a tiny bug in line 61. The parameter isn't the textView, it's actually an NSNotification so the parameter should be sender: NSNotification. And since we have that object it's probably better to actually use the sender's variables instead of self's, something like this:

@objc public func textViewDidChange(_ sender: NSNotification) {
    guard let textView = sender.object as? UITextView else { return }
    if let placeholderLabel = textView.viewWithTag(100) as? UILabel {
        placeholderLabel.isHidden = !textView.text.isEmpty
    }
}
@tijme

This comment has been minimized.

Copy link
Owner Author

tijme commented Nov 9, 2017

@manolosavi Thanks! I think you're not right about the bug though. I checked the documentation and it seems like the argument is a UITextView.

@manolosavi

This comment has been minimized.

Copy link

manolosavi commented Nov 16, 2017

@tijme sorry for the late reply, not sure why I never got a notification. That's odd, not sure what's going on because in my tests what I was getting in there was an NSNotification, in fact because of the guard there it's the only way this would be working, if sender was already a UITextView then it wouldn't pass the guard and return there. I put a breakpoint in that function and po textView [now sender] and the console showed it's a notification…

edit: nevermind I just saw a difference in your code vs mine, I copied from a different fork that uses the notification style…my bad!

@TedHopp

This comment has been minimized.

Copy link

TedHopp commented Apr 20, 2018

@manolosavi For the notification version (which, in my view, is an important improvement over hijacking the delegate), it would be better to set the last argument to self instead of nil, so the selector only gets called for changes to the text of our own text, not every UITextView instance. Otherwise I think every instance will have its own textViewDidChange method invoked when any text changes in any instance.

@vineet-devin

This comment has been minimized.

Copy link

vineet-devin commented May 25, 2018

@manolosavi Can u please give me a link to that gist which uses NSNotification? I want to know where the observer has been removed.

@zhiyilee

This comment has been minimized.

Copy link

zhiyilee commented Jun 4, 2018

@tijme Thanks for your great extension then how can i remove placeholder at adding text programatically. for instance, mTextView.text = "Ok but i can't remove placeholder :( ".

@yesleon

This comment has been minimized.

Copy link

yesleon commented Jun 4, 2018

You can use NSTextStorageDelegate instead of UITextViewDelegate or UITextViewTextDidChange notification to observe all changes made to the text property. Plus you can use the UITextViewDelegate somewhere else.

@caioberkley

This comment has been minimized.

Copy link

caioberkley commented Jan 30, 2019

"self.text.characters.count" is deprecated in Swift 4.2 and in the future may be disabled in turn. Xcode suggests changing "characters" to "string" or "substring" but it does not work. I was able to silence the warning by removing the "characters" without replacing with anything as can be seen in the attached example.

captura de tela 2019-01-30 as 11 37 14

captura de tela 2019-01-30 as 11 40 10

P.S .: Thanks for share this code, it helped me a lot.

@yuvalt

This comment has been minimized.

Copy link

yuvalt commented Mar 18, 2019

Thanks for this. Two comments:

  1. Missing: placeholderLabel.numberOfLines = 0 for longer placeholders.
  2. You should call placeholderLabel.sizeToFit() after setting the placeholderLabel.frame size.
@shafqat-muneer

This comment has been minimized.

Copy link

shafqat-muneer commented Jun 19, 2019

If you enter text programmatically then placeholder text not removed. I did following changes to make it work:

NotificationCenter.default.addObserver(self, selector: #selector(textViewDidChangeSelection), name: NSNotification.Name(rawValue: "TextViewDidChangeSelection"), object: nil)

When we enter text programmatically then textViewDidChangeSelection delegate method called; Here we can post notification. (This delegate method called in my app code because i need some custom logic in delegates)
func textViewDidChangeSelection(_ textView: UITextView) { NotificationCenter.default.post(name: Notification.Name("TextViewDidChangeSelection"), object: nil) }

Note: I removed self.delegate = self because, i need delegate methods for business logic; due to that, delegate methods were not calling.

@jitendragaur

This comment has been minimized.

Copy link

jitendragaur commented Jul 3, 2019

@germs5 If you want to support multiline placeholder text then you just need to add below line of code in addPlaceholder function.

placeholderLabel.frame = self.frame

@bogren

This comment has been minimized.

Copy link

bogren commented Oct 14, 2019

A small improvement to take insets into consideration:

let labelX = self.textContainerInset.left + textContainer.lineFragmentPadding

https://gist.github.com/tijme/14ec04ef6a175a70dd5a759e7ff0b938#file-uitextviewplaceholder-swift-L70

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.