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
}
}
@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