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.

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.

Owner

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.

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.

Owner

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.

@liaa

This comment has been minimized.

liaa 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.

Owner

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.

congnd commented Jun 5, 2017

what is the license of this lib?

@tijme

This comment has been minimized.

Owner

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.

astrokin commented Jul 16, 2017

what if i need delegate somewhere else?

@milsawicki

This comment has been minimized.

milsawicki commented Jul 24, 2017

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

@seubseub

This comment has been minimized.

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.

Owner

tijme commented Aug 18, 2017

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

@yuvalt

This comment has been minimized.

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.

Owner

tijme commented Sep 5, 2017

@yuvalt Ah I see, very nice!

@manolosavi

This comment has been minimized.

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.

Owner

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.

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.

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.

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.

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.

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.

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