Skip to content

Instantly share code, notes, and snippets.

@tijme
Last active January 7, 2024 03:06
Show Gist options
  • Star 54 You must be signed in to star a gist
  • Fork 22 You must be signed in to fork a gist
  • Save tijme/14ec04ef6a175a70dd5a759e7ff0b938 to your computer and use it in GitHub Desktop.
Save tijme/14ec04ef6a175a70dd5a759e7ff0b938 to your computer and use it in GitHub Desktop.
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.isEmpty
}
}
/// 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.isEmpty
self.addSubview(placeholderLabel)
self.resizePlaceholder()
self.delegate = self
}
}
@germs5
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
Copy link
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
Copy link

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
Copy link
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
Copy link

congnd commented Jun 5, 2017

what is the license of this lib?

@tijme
Copy link
Author

tijme commented Jun 5, 2017

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

@astrokin
Copy link

what if i need delegate somewhere else?

@milsawicki
Copy link

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

@seubseub
Copy link

@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
Copy link
Author

tijme commented Aug 18, 2017

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

@yuvalt
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
Copy link
Author

tijme commented Sep 5, 2017

@yuvalt Ah I see, very nice!

@manolosavi
Copy link

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
Copy link
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
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
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
Copy link

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

@darkhorse-coder
Copy link

@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
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
Copy link

"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
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
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
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
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

@katleta3000
Copy link

Hy! Thx for the gist.
It's better to change self.text.characters.count > 0 to !self.text.isEmpty.
The code doesn't compile on Swift 5.2, also it's better not to use any chasets to detect whether the string is empty.

@Usmaan95
Copy link

Hi, I am new to this so please be nice lol but how do you actually use this???

@katleta3000
Copy link

Yeah, it should work with my changes

@tijme
Copy link
Author

tijme commented Apr 27, 2020

Hi, I am new to this so please be nice lol but how do you actually use this???

You can place this file in your project and then set do yourTextView.placeholder = 'test...'.

@MoNTE48
Copy link

MoNTE48 commented Jul 3, 2020

Just wanted to say thank you for such a simple solution.

@tijme
Copy link
Author

tijme commented Jul 3, 2020

Thanks @MoNTE48!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment