754

My application uses an UITextView. Now I want the UITextView to have a placeholder similar to the one you can set for an UITextField.

How to do this?

pkamb
  • 33,281
  • 23
  • 160
  • 191
  • [Here's small and awesome solution for this](http://stackoverflow.com/a/7038941/1229954) – Hemang Mar 20 '12 at 07:08
  • 24
    How about using UITextView+Placeholder category? https://github.com/devxoul/UITextView-Placeholder – devxoul Feb 27 '15 at 10:21
  • 2
    I favor @devxoul's solution coz it uses category, not subclass. And it also creates a field for 'placeholder' options (placeholder text & text color) in IB's inspector. It uses some binding techniques. What a great code – samthui7 Oct 21 '16 at 07:03
  • If you're using `UITextView` the solution becomes quite different. Here are a couple of solutions. [Floating Placeholder](http://stackoverflow.com/a/28271069/2079103) and [Fake Native Placeholders](http://stackoverflow.com/questions/27652227/text-view-placeholder-swift/41081244#41081244) – clearlight Jan 08 '17 at 01:00
  • Try this answer :) [_textView setTextColor:[UIColor lightGrayColor]]; – Kingsley Mitchell Mar 19 '17 at 22:21
  • @ICoffeeConsumer What's interesting is that the native iOS Calendar app has multi-line editable text - with a placeholder (adding 'Notes' to an event). So there's some direct evidence that Apple themselves recognise the need/use for it. – wardw Jul 29 '18 at 10:30
  • Three20's TTTextEditor (itself using UITextField) supports placeholder text as well as growing by height (it turns into a UITextView). – Josh Benjamin Aug 04 '11 at 03:16

63 Answers63

676

I made a few minor modifications to bcd's solution to allow for initialization from a Xib file, text wrapping, and to maintain background color. Hopefully it will save others the trouble.

UIPlaceHolderTextView.h:

#import <Foundation/Foundation.h>
IB_DESIGNABLE
@interface UIPlaceHolderTextView : UITextView

@property (nonatomic, retain) IBInspectable NSString *placeholder;
@property (nonatomic, retain) IBInspectable UIColor *placeholderColor;

-(void)textChanged:(NSNotification*)notification;

@end

UIPlaceHolderTextView.m:

#import "UIPlaceHolderTextView.h"

@interface UIPlaceHolderTextView ()

@property (nonatomic, retain) UILabel *placeHolderLabel;

@end

@implementation UIPlaceHolderTextView

CGFloat const UI_PLACEHOLDER_TEXT_CHANGED_ANIMATION_DURATION = 0.25;

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
#if __has_feature(objc_arc)
#else
    [_placeHolderLabel release]; _placeHolderLabel = nil;
    [_placeholderColor release]; _placeholderColor = nil;
    [_placeholder release]; _placeholder = nil;
    [super dealloc];
#endif
}

- (void)awakeFromNib
{
    [super awakeFromNib];

    // Use Interface Builder User Defined Runtime Attributes to set
    // placeholder and placeholderColor in Interface Builder.
    if (!self.placeholder) {
        [self setPlaceholder:@""];
    }

    if (!self.placeholderColor) {
        [self setPlaceholderColor:[UIColor lightGrayColor]];
    }

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textChanged:) name:UITextViewTextDidChangeNotification object:nil];
}

- (id)initWithFrame:(CGRect)frame
{
    if( (self = [super initWithFrame:frame]) )
    {
        [self setPlaceholder:@""];
        [self setPlaceholderColor:[UIColor lightGrayColor]];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textChanged:) name:UITextViewTextDidChangeNotification object:nil];
    }
    return self;
}

- (void)textChanged:(NSNotification *)notification
{
    if([[self placeholder] length] == 0)
    {
        return;
    }

    [UIView animateWithDuration:UI_PLACEHOLDER_TEXT_CHANGED_ANIMATION_DURATION animations:^{
    if([[self text] length] == 0)
    {
        [[self viewWithTag:999] setAlpha:1];
    }
    else
    {
        [[self viewWithTag:999] setAlpha:0];
    }
    }];
}

- (void)setText:(NSString *)text {
    [super setText:text];
    [self textChanged:nil];
}

- (void)drawRect:(CGRect)rect
{
    if( [[self placeholder] length] > 0 )
    {
        if (_placeHolderLabel == nil )
        {
            _placeHolderLabel = [[UILabel alloc] initWithFrame:CGRectMake(8,8,self.bounds.size.width - 16,0)];
            _placeHolderLabel.lineBreakMode = NSLineBreakByWordWrapping;
            _placeHolderLabel.numberOfLines = 0;
            _placeHolderLabel.font = self.font;
            _placeHolderLabel.backgroundColor = [UIColor clearColor];
            _placeHolderLabel.textColor = self.placeholderColor;
            _placeHolderLabel.alpha = 0;
            _placeHolderLabel.tag = 999;
            [self addSubview:_placeHolderLabel];
        }

        _placeHolderLabel.text = self.placeholder;
        [_placeHolderLabel sizeToFit];
        [self sendSubviewToBack:_placeHolderLabel];
    }

    if( [[self text] length] == 0 && [[self placeholder] length] > 0 )
    {
        [[self viewWithTag:999] setAlpha:1];
    }

    [super drawRect:rect];
}

@end
Randika Vishman
  • 7,983
  • 3
  • 57
  • 80
Jason George
  • 6,992
  • 8
  • 44
  • 60
  • Each time drawRect is called a new UILabel is added to the view's hierarchy. I've not done a lot around drawRect -- is the assumption that there is no persistence in the view heararchy, so that each time drawRect is called the previously added label subview has been released? – memmons Mar 17 '11 at 20:09
  • Overall a good implementation, but I have to agree with Sam Soffes's answer -- adding a label to the view hierarchy is quite heavy for what is needed. In addition his implementation of `_updateShouldDrawPlaceholder` and use of `setNeedsDisplay` is more thorough. – memmons Mar 17 '11 at 20:17
  • 2
    in some cases (esp. iOS 5 compatibility) it is required to override paste: - (void)paste:(id)sender { [super paste:sender]; [self textChanged:nil]; } – Martin Ullrich Jul 14 '11 at 15:32
  • 3
    Good stuff! Reminder about best practices for NSString (or any class that has a NSMutableXXX equivalent), property should be "copy" and not "retain". – Oli Jul 28 '11 at 20:37
  • @gamma: Couldn't the duplicate code in initWithFrame: and awakeFromNib: be put in a single method (to ease any additional styling that one might do)? Or does that have extra consequences? – SpacyRicochet Sep 05 '11 at 13:01
  • @SpacyRicochet I guess so (I just took the original code from Jason George ;)) - but it wouldn't really spare you anything. I think the code is quite readable the way it is. – gamma Sep 05 '11 at 14:45
  • 2
    How do you instantiate this code? I don't see any placeholder text and nothing clears when I start typing. – user798719 Dec 20 '12 at 23:17
  • @JasonGeorge you need to change line breaker to [placeHolderLabel setLineBreakMode:NSLineBreakByWordWrapping]; in IOs6 – Dejell Jan 27 '13 at 08:40
  • Would it be better to check for placeholder == nil before setting @"". In interface builder you can use the User Defined Runtime Attributes to set the placeholder (and placeholderColor) and they currently get overridden by the awakeFromNib method. I changed mine. – Fogmeister Apr 18 '13 at 09:06
  • 1
    @RazorSharp Try to use: `((UIPlaceHolderTextView *)MyTextView.placeholder).placeholder = @"blabla";` – Jeremy Jul 29 '13 at 09:30
  • 1
    @RazorSharp jeremy answer gave me sigAbort , so i tried instance of UIPlaceHolderTextView eg:- profleBio = [[UIPlaceHolderTextView alloc] init...and [profleBio setPlaceholder:@"Blah"]; – iMeMyself Aug 01 '13 at 06:41
  • 41
    This is a very, very poorly written implementation. Here's a vastly cleaner version that also watches for dictation changes: https://github.com/cbowns/MPTextView – cbowns Aug 02 '13 at 17:54
  • I though you can add place holder in IB. – Bagusflyer Oct 27 '13 at 02:44
  • 11
    Do not modify the view hierarchy in drawRect. – Karmeye Oct 31 '13 at 13:12
  • This practice can lead to some bugs. As far as I know, this can break the animation in the `backgroundColor` property somehow. I guess it's because you modify the view hierarchy in drawRect. – Henry H Miao Jun 13 '14 at 03:19
  • 1
    To deal with not only `text` but also properties such as `font` or `textAlignment`, you can try this category: https://github.com/devxoul/UITextView-Placeholder – devxoul Feb 27 '15 at 10:22
  • The placeholder won't update when if it's set again by code. Override the setter to fix this: `- (void)setPlaceholder:(NSString *)placeholder { _placeholder = placeholder; [self setNeedsDisplay]; }` – Justin Jul 16 '15 at 00:08
  • You can use another UITextView instead of UILabel to match text insets. – Uladzimir Aug 06 '15 at 09:52
  • Unfortunately, @cbowns MPTextView repo is no longer being maintained. – ThomasW Aug 14 '15 at 08:25
  • Tip: you can add `IBDesignable` to the .h properties (just before the type declaration) to be able to set the placeholder text and color in Storyboard! – gklka Aug 20 '15 at 13:55
  • 2
    I don't understand why Apple has not added a placeholder property to UITextView. I don't get it. – crubio Nov 03 '15 at 11:33
  • contains too much logical errors. In my case I need to make the label public because the frame doesn't properly work. Then I need to setup constraints - they won't work by default too because of silly label initialization (inside drawRect and is not created if placeholder is empty) and etc. – Vyachaslav Gerchicov Jan 02 '19 at 15:43
643

Easy way, just create placeholder text in UITextView by using the following UITextViewDelegate methods:

- (void)textViewDidBeginEditing:(UITextView *)textView
{
    if ([textView.text isEqualToString:@"placeholder text here..."]) {
         textView.text = @"";
         textView.textColor = [UIColor blackColor]; //optional
    }
    [textView becomeFirstResponder];
}

- (void)textViewDidEndEditing:(UITextView *)textView
{
    if ([textView.text isEqualToString:@""]) {
        textView.text = @"placeholder text here...";
        textView.textColor = [UIColor lightGrayColor]; //optional
    }
    [textView resignFirstResponder];
}

just remember to set myUITextView with the exact text on creation e.g.

UITextView *myUITextView = [[UITextView alloc] init];
myUITextView.delegate = self;
myUITextView.text = @"placeholder text here...";
myUITextView.textColor = [UIColor lightGrayColor]; //optional

and make the parent class a UITextViewDelegate before including these methods e.g.

@interface MyClass () <UITextViewDelegate>
@end

Code for Swift 3.1

func textViewDidBeginEditing(_ textView: UITextView) 
{
    if (textView.text == "placeholder text here..." && textView.textColor == .lightGray)
    {
        textView.text = ""
        textView.textColor = .black
    }
    textView.becomeFirstResponder() //Optional
}

func textViewDidEndEditing(_ textView: UITextView)
{
    if (textView.text == "")
    {
        textView.text = "placeholder text here..."
        textView.textColor = .lightGray
    }
    textView.resignFirstResponder()
}

just remember to set myUITextView with the exact text on creation e.g.

 let myUITextView = UITextView.init()
 myUITextView.delegate = self
 myUITextView.text = "placeholder text here..."
 myUITextView.textColor = .lightGray

and make the parent class a UITextViewDelegate before including these methods e.g.

class MyClass: UITextViewDelegate
{

}
dvp.petrov
  • 1,110
  • 13
  • 20
CmKndy
  • 1,341
  • 1
  • 9
  • 6
  • 1
    This is great (I love simple) for 1 screen with 1 UITextView. The reason for the more complicated solutions is that if you have a larger app with MANY screens and MANY UITextViews, you don't want to do this over and over. You probably want to subclass UITextView to fit your needs and then use that. – ghostatron Jan 24 '13 at 02:14
  • 43
    If someone types "placeholder text here..." in the textbox it also behaves like placeholder text. Also, during submission you will need to check for all those criteria. – Anindya Sengupta Sep 18 '13 at 15:18
  • 1
    agree with @jklp, those up-voted over-engineered methods are not as nice as this one. – Tony Xu Nov 14 '13 at 01:36
  • This breaks `enablesReturnKeyAutomatically`. :( – Rudolf Adamkovič Jan 04 '14 at 13:10
  • 7
    The placeholder text is supposed to show even when the field becomes the responder, and this method will not work for this. – phatmann Jan 06 '14 at 14:35
  • 1
    Use [UIColor colorWithWhite:0.8 alpha:1.0] for same color as UITextField placeholder color on iOS 7. – Fogh Feb 25 '14 at 10:30
  • 18
    @jklp I would argue that the "over-engineered" way is cleaner and more re-usable...and it looks like it doesn't tamper with the `text` attribute of the textview which is kinda nice..whereas this method modifies it – Cameron Askew Apr 06 '14 at 21:26
  • This will exhibit odd behavior if the user taps on the placeholder text. Generally you want the text to disappear if that happens but this assumes the user will only be typing. Not a huge problem but a noticeable subtlety that made me chose Sam Soffes solution. – Usman Mutawakil May 05 '14 at 17:41
  • @phatmann - But that ignores the fact that continuing to show the placeholder text after the field gains focus is a _very stupid_ convention that only became commonplace because it's what Apple picked. – aroth Jul 25 '14 at 04:49
  • @aroth I don't like this behavior either, but the OP said they wanted a placeholder "similar to the one you can set for an UITextField". That implies they want their placeholder to behave as much like UITextField's as possible. – phatmann Jul 27 '14 at 11:04
  • 3
    why the calls to becomeFirstResponder and resignFirstResponder in the delegate methods? – Adam Johns Mar 02 '15 at 21:11
  • Thought this was good - however doesn't have the required UX - i.e. placeholder text remains upon immediate focus and no entered text, reappears when there is no entered text. Nice try though :) – dijipiji Jun 22 '15 at 09:52
119

I wasn't too happy with any of the solutions posted as they were a bit heavy. Adding views to the view isn't really ideal (especially in drawRect:). They both had leaks, which isn't acceptable either.

Here is my solution: SAMTextView

SAMTextView.h

//
//  SAMTextView.h
//  SAMTextView
//
//  Created by Sam Soffes on 8/18/10.
//  Copyright 2010-2013 Sam Soffes. All rights reserved.
//

#import <UIKit/UIKit.h>

/**
 UITextView subclass that adds placeholder support like UITextField has.
 */
@interface SAMTextView : UITextView

/**
 The string that is displayed when there is no other text in the text view.

 The default value is `nil`.
 */
@property (nonatomic, strong) NSString *placeholder;

/**
 The color of the placeholder.

 The default is `[UIColor lightGrayColor]`.
 */
@property (nonatomic, strong) UIColor *placeholderTextColor;

/**
 Returns the drawing rectangle for the text views’s placeholder text.

 @param bounds The bounding rectangle of the receiver.
 @return The computed drawing rectangle for the placeholder text.
 */
- (CGRect)placeholderRectForBounds:(CGRect)bounds;

@end

SAMTextView.m

//
//  SAMTextView.m
//  SAMTextView
//
//  Created by Sam Soffes on 8/18/10.
//  Copyright 2010-2013 Sam Soffes. All rights reserved.
//

#import "SAMTextView.h"

@implementation SAMTextView

#pragma mark - Accessors

@synthesize placeholder = _placeholder;
@synthesize placeholderTextColor = _placeholderTextColor;

- (void)setText:(NSString *)string {
  [super setText:string];
  [self setNeedsDisplay];
}


- (void)insertText:(NSString *)string {
  [super insertText:string];
  [self setNeedsDisplay];
}


- (void)setAttributedText:(NSAttributedString *)attributedText {
  [super setAttributedText:attributedText];
  [self setNeedsDisplay];
}


- (void)setPlaceholder:(NSString *)string {
  if ([string isEqual:_placeholder]) {
    return;
  }

  _placeholder = string;
  [self setNeedsDisplay];
}


- (void)setContentInset:(UIEdgeInsets)contentInset {
  [super setContentInset:contentInset];
  [self setNeedsDisplay];
}


- (void)setFont:(UIFont *)font {
  [super setFont:font];
  [self setNeedsDisplay];
}


- (void)setTextAlignment:(NSTextAlignment)textAlignment {
  [super setTextAlignment:textAlignment];
  [self setNeedsDisplay];
}


#pragma mark - NSObject

- (void)dealloc {
  [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidChangeNotification object:self];
}


#pragma mark - UIView

- (id)initWithCoder:(NSCoder *)aDecoder {
  if ((self = [super initWithCoder:aDecoder])) {
    [self initialize];
  }
  return self;
}


- (id)initWithFrame:(CGRect)frame {
  if ((self = [super initWithFrame:frame])) {
    [self initialize];
  }
  return self;
}


- (void)drawRect:(CGRect)rect {
  [super drawRect:rect];

  if (self.text.length == 0 && self.placeholder) {
    rect = [self placeholderRectForBounds:self.bounds];

    UIFont *font = self.font ? self.font : self.typingAttributes[NSFontAttributeName];

    // Draw the text
    [self.placeholderTextColor set];
    [self.placeholder drawInRect:rect withFont:font lineBreakMode:NSLineBreakByTruncatingTail alignment:self.textAlignment];
  }
}


#pragma mark - Placeholder

- (CGRect)placeholderRectForBounds:(CGRect)bounds {
  // Inset the rect
  CGRect rect = UIEdgeInsetsInsetRect(bounds, self.contentInset);

  if (self.typingAttributes) {
    NSParagraphStyle *style = self.typingAttributes[NSParagraphStyleAttributeName];
    if (style) {
      rect.origin.x += style.headIndent;
      rect.origin.y += style.firstLineHeadIndent;
    }
  }

  return rect;
}


#pragma mark - Private

- (void)initialize {
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textChanged:) name:UITextViewTextDidChangeNotification object:self];

  self.placeholderTextColor = [UIColor colorWithWhite:0.702f alpha:1.0f];
}


- (void)textChanged:(NSNotification *)notification {
  [self setNeedsDisplay];
}

@end

It's a lot simpler than the others, as it doesn't use subviews (or have leaks). Feel free to use it.

Update 11/10/11: It is now documented and supports use in Interface Builder.

Update 11/24/13: Point to new repo.

Sam Soffes
  • 14,831
  • 9
  • 76
  • 80
  • I like your solution, and I added an override of `setText` to also update placeholder when changing text property programatically: - (void)setText:(NSString *)string { [super setText:string]; [self _updateShouldDrawPlaceholder]; } – olegueret Sep 08 '10 at 09:29
  • 1
    I like your solution too but you've missed the awakefromnib method so your init method won't always get called. I took it from one of the others here. – toxaq Dec 06 '10 at 16:23
  • Note -- the magic numbers will vary depending on font size. The exact positioning can be calculated from the font, but probably isn't worth it for this implementation. The correct positioning of the placeholder should be 2px to the right of the cursor position when there is no text. – memmons Mar 17 '11 at 21:25
  • To solve the loading from nib: you probably want to add an - (id) initWithCoder:(NSCoder *)aDecoder; initializer to accompany the existing one. – Joris Kluivers May 04 '11 at 11:21
  • This is sweet. The `notification` argument is misspelled in the last method, though, and it's too small a change to submit as an edit. – Phil Calvin May 11 '12 at 18:46
  • I like the approach. I ported it to Swift/iOS 8 and posted it below. Few tweaks but it works for me with a basic UITextView so far. Thanks – clearlight Jan 02 '15 at 02:46
  • This is not working in iOS 9.2, I would go with this https://github.com/gcamp/GCPlaceholderTextView (next answer), it works perfectly and you can set placeholder text and placeholder text color. – Boris Nikolic Jan 08 '16 at 15:08
56

I found myself a very easy way to imitate a place-holder

  1. in the NIB or code set your textView's textColor to lightGrayColor (most of the time)
  2. make sure that your textView's delegate is linked to file's owner and implement UITextViewDelegate in your header file
  3. set the default text of your text view to (example: "Foobar placeholder")
  4. implement: (BOOL) textViewShouldBeginEditing:(UITextView *)textView

Edit:

Changed if statements to compare tags rather than text. If the user deleted their text it was possible to also accidentally delete a portion of the place holder @"Foobar placeholder".This meant if the user re-entered the textView the following delegate method, -(BOOL) textViewShouldBeginEditing:(UITextView *) textView, it would not work as expected. I tried comparing by the color of the text in the if statement but found that light grey color set in interface builder is not the same as light grey color set in code with [UIColor lightGreyColor]

- (BOOL) textViewShouldBeginEditing:(UITextView *)textView
{
    if(textView.tag == 0) {
        textView.text = @"";
        textView.textColor = [UIColor blackColor];
        textView.tag = 1;
    }
    return YES;
}

It is also possible to reset the placeholder text when the keyboard returns and the [textView length] == 0

EDIT:

Just to make the last part clearer - here's is how you can set the placeholder text back:

- (void)textViewDidChange:(UITextView *)textView
{
   if([textView.text length] == 0)
   {
       textView.text = @"Foobar placeholder";
       textView.textColor = [UIColor lightGrayColor];
       textView.tag = 0;
   }
}
Nilesh R Patel
  • 697
  • 7
  • 17
vdevos
  • 491
  • 7
  • 11
  • 15
    I like this approach very much! The only thing that I would do to the edit above would be to move the implementation out of the textViewDidChange: method and into the textViewDidEndEditing: method, so that the placeholder text only returns once you're finished working with the object. – horseshoe7 Apr 01 '12 at 19:54
  • Use this https://cocoapods.org/pods/UITextView+Placeholder – Abhishek Bedi Feb 01 '21 at 08:46
52

What you can do is set up the text view with some initial value in the text property, and change the textColor to [UIColor grayColor] or something similar. Then, whenever the text view becomes editable, clear the text and present a cursor, and if the text field is ever empty again, put your placeholder text back. Change the color to [UIColor blackColor] as appropriate.

It's not exactly the same as the placeholder functionality in a UITextField, but it's close.

Tim
  • 59,527
  • 19
  • 156
  • 165
  • 9
    I've always used _lightGrayColor_, which seems to match the color of the placeholder text. – Bill Nov 08 '10 at 01:35
  • I'm just reading this now, but I do want to add that resetting the colour to black and resetting the text property in textViewShouldBeginEditing:(UITextView *)textView works very well. This is a very nice and quick solution compared to the solutions below (but still elegant solutions below, subclassing uitextview. Much more modular). – Enrico Susatyo Apr 13 '11 at 00:25
  • 3
    True, but it doesn't mimic the behaviour of UITextField, which only replaces its placeholder text when the user types something, not when editing begins, and which adds the placeholder back again the second the view is empty, not when editing actually finishes. – Ash Mar 13 '12 at 10:20
49

You can set the label on the UITextView by

[UITextView addSubView:lblPlaceHoldaer];

and hide it on TextViewdidChange method.

This is the simple & easy way.

sth
  • 222,467
  • 53
  • 283
  • 367
MohammedYakub M.
  • 2,893
  • 5
  • 31
  • 42
49

Simple Swift 3 solution

Add UITextViewDelegate to your class

Set yourTextView.delegate = self

Create placeholderLabel and position it inside yourTextView

Now just animate placeholderLabel.alpha on textViewDidChange:

  func textViewDidChange(_ textView: UITextView) {
    let newAlpha: CGFloat = textView.text.isEmpty ? 1 : 0
    if placeholderLabel.alpha != newAlpha {
      UIView.animate(withDuration: 0.3) {
        self.placeholderLabel.alpha = newAlpha
      }
    }
  }

you might have to play with placeholderLabel position to set it up right, but that shouldn't be too hard

budiDino
  • 13,044
  • 8
  • 95
  • 91
  • 1
    Great answer, simple solution. I added a small improvement to animate only when alpha should change: `let alpha = CGFloat(textView.text.isEmpty ? 1.0 : 0.0) if alpha != lblPlaceholder.alpha { UIView.animate(withDuration: 0.3) { self.lblPlaceholder.alpha = alpha } }` – Luciano Sclovsky May 01 '18 at 14:10
46

If someone needs a Solution for Swift:

Add UITextViewDelegate to your class

var placeHolderText = "Placeholder Text..."

override func viewDidLoad() {
    super.viewDidLoad()
    textView.delegate = self
}

func textViewShouldBeginEditing(textView: UITextView) -> Bool {

    self.textView.textColor = .black

    if(self.textView.text == placeHolderText) {
        self.textView.text = ""
    }

    return true
}

func textViewDidEndEditing(textView: UITextView) {
    if(textView.text == "") {
        self.textView.text = placeHolderText
        self.textView.textColor = .lightGray
    }
}

override func viewWillAppear(animated: Bool) {

    if(currentQuestion.answerDisplayValue == "") {
        self.textView.text = placeHolderText
        self.textView.textColor = .lightGray
    } else {
        self.textView.text = "xxx" // load default text / or stored 
        self.textView.textColor = .black
    }
}
Alain Stulz
  • 715
  • 6
  • 19
derdida
  • 14,784
  • 16
  • 90
  • 139
  • 1
    this is ok but not good enough. if user types "Placeholder Text..." (edge case obviously), it breaks your logic – Lucas Chwe Sep 19 '18 at 20:39
25

I extended KmKndy's answer, so that the placeholder remains visible until the user starts editing the UITextView rather than just taps on it. This mirrors the functionality in the Twitter and Facebook apps. My solution doesn't require you to subclass and works if the user types directly or pastes text!

Example of Placeholder Twitter App

- (void)textViewDidChangeSelection:(UITextView *)textView{
    if ([textView.text isEqualToString:@"What's happening?"] && [textView.textColor isEqual:[UIColor lightGrayColor]])[textView setSelectedRange:NSMakeRange(0, 0)];

}

- (void)textViewDidBeginEditing:(UITextView *)textView{

    [textView setSelectedRange:NSMakeRange(0, 0)];
}

- (void)textViewDidChange:(UITextView *)textView
{
    if (textView.text.length != 0 && [[textView.text substringFromIndex:1] isEqualToString:@"What's happening?"] && [textView.textColor isEqual:[UIColor lightGrayColor]]){
        textView.text = [textView.text substringToIndex:1];
        textView.textColor = [UIColor blackColor]; //optional

    }
    else if(textView.text.length == 0){
        textView.text = @"What's happening?";
        textView.textColor = [UIColor lightGrayColor];
        [textView setSelectedRange:NSMakeRange(0, 0)];
    }
}

- (void)textViewDidEndEditing:(UITextView *)textView
{
    if ([textView.text isEqualToString:@""]) {
        textView.text = @"What's happening?";
        textView.textColor = [UIColor lightGrayColor]; //optional
    }
    [textView resignFirstResponder];
}

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text{
    if (textView.text.length > 1 && [textView.text isEqualToString:@"What's happening?"]) {
         textView.text = @"";
         textView.textColor = [UIColor blackColor];
    }

    return YES;
}

just remember to set myUITextView with the exact text on creation e.g.

UITextView *myUITextView = [[UITextView alloc] init];
myUITextView.delegate = self;
myUITextView.text = @"What's happening?";
myUITextView.textColor = [UIColor lightGrayColor]; //optional

and make the parent class a UITextView delegate before including these methods e.g.

@interface MyClass () <UITextViewDelegate>
@end
Community
  • 1
  • 1
Matt
  • 1,359
  • 18
  • 22
22

Below is a Swift port of "SAMTextView" ObjC code posted as one of the first handful of replies to the question. I tested it on iOS 8. I tweaked a couple of things, including the bounds offset for the placement of the placeholder text, as the original was too high and too far right (used suggestion in one of the comments to that post).

I know there are a lot of simple solutions, but I like the approach of subclassing UITextView because it's reusable and I don't have to clutter classes utilizing it with the mechanisms.

Swift 2.2:

import UIKit

class PlaceholderTextView: UITextView {

    @IBInspectable var placeholderColor: UIColor = UIColor.lightGrayColor()
    @IBInspectable var placeholderText: String = ""

    override var font: UIFont? {
        didSet {
            setNeedsDisplay()
        }
    }

    override var contentInset: UIEdgeInsets {
        didSet {
            setNeedsDisplay()
        }
    }

    override var textAlignment: NSTextAlignment {
        didSet {
            setNeedsDisplay()
        }
    }

    override var text: String? {
        didSet {
            setNeedsDisplay()
        }
    }

    override var attributedText: NSAttributedString? {
        didSet {
            setNeedsDisplay()
        }
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setUp()
    }

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
    }

    private func setUp() {
        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(PlaceholderTextView.textChanged(_:)),
                                                         name: UITextViewTextDidChangeNotification, object: self)
    }

    func textChanged(notification: NSNotification) {
        setNeedsDisplay()
    }

    func placeholderRectForBounds(bounds: CGRect) -> CGRect {
        var x = contentInset.left + 4.0
        var y = contentInset.top  + 9.0
        let w = frame.size.width - contentInset.left - contentInset.right - 16.0
        let h = frame.size.height - contentInset.top - contentInset.bottom - 16.0

        if let style = self.typingAttributes[NSParagraphStyleAttributeName] as? NSParagraphStyle {
            x += style.headIndent
            y += style.firstLineHeadIndent
        }
        return CGRect(x: x, y: y, width: w, height: h)
    }

    override func drawRect(rect: CGRect) {
        if text!.isEmpty && !placeholderText.isEmpty {
            let paragraphStyle = NSMutableParagraphStyle()
            paragraphStyle.alignment = textAlignment
            let attributes: [ String: AnyObject ] = [
                NSFontAttributeName : font!,
                NSForegroundColorAttributeName : placeholderColor,
                NSParagraphStyleAttributeName  : paragraphStyle]

            placeholderText.drawInRect(placeholderRectForBounds(bounds), withAttributes: attributes)
        }
        super.drawRect(rect)
    }
}

Swift 4.2:

import UIKit

class PlaceholderTextView: UITextView {

    @IBInspectable var placeholderColor: UIColor = UIColor.lightGray
    @IBInspectable var placeholderText: String = ""

    override var font: UIFont? {
        didSet {
            setNeedsDisplay()
        }
    }

    override var contentInset: UIEdgeInsets {
        didSet {
            setNeedsDisplay()
        }
    }

    override var textAlignment: NSTextAlignment {
        didSet {
            setNeedsDisplay()
        }
    }

    override var text: String? {
        didSet {
            setNeedsDisplay()
        }
    }

    override var attributedText: NSAttributedString? {
        didSet {
            setNeedsDisplay()
        }
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setUp()
    }

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
    }

    private func setUp() {
        NotificationCenter.default.addObserver(self,
         selector: #selector(self.textChanged(notification:)),
         name: Notification.Name("UITextViewTextDidChangeNotification"),
         object: nil)
    }

    @objc func textChanged(notification: NSNotification) {
        setNeedsDisplay()
    }

    func placeholderRectForBounds(bounds: CGRect) -> CGRect {
        var x = contentInset.left + 4.0
        var y = contentInset.top  + 9.0
        let w = frame.size.width - contentInset.left - contentInset.right - 16.0
        let h = frame.size.height - contentInset.top - contentInset.bottom - 16.0

        if let style = self.typingAttributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle {
            x += style.headIndent
            y += style.firstLineHeadIndent
        }
        return CGRect(x: x, y: y, width: w, height: h)
    }

    override func draw(_ rect: CGRect) {
        if text!.isEmpty && !placeholderText.isEmpty {
            let paragraphStyle = NSMutableParagraphStyle()
            paragraphStyle.alignment = textAlignment
            let attributes: [NSAttributedString.Key: Any] = [
            NSAttributedString.Key(rawValue: NSAttributedString.Key.font.rawValue) : font!,
            NSAttributedString.Key(rawValue: NSAttributedString.Key.foregroundColor.rawValue) : placeholderColor,
            NSAttributedString.Key(rawValue: NSAttributedString.Key.paragraphStyle.rawValue)  : paragraphStyle]

            placeholderText.draw(in: placeholderRectForBounds(bounds: bounds), withAttributes: attributes)
        }
        super.draw(rect)
    }
}
Mario Huizinga
  • 780
  • 7
  • 14
clearlight
  • 12,255
  • 11
  • 57
  • 75
21

I recommend to use SZTextView.

https://github.com/glaszig/SZTextView

Add your default UITextView from storyboard and then change its custom class to SZTextView like below

enter image description here

Then you will see two new option in the Attribute Inspector

enter image description here

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Vaibhav Saran
  • 12,848
  • 3
  • 65
  • 75
15

Hi you can use IQTextView available in IQKeyboard Manager it's simple to use and integrate just set class of your textview to IQTextView and you can use its property for setting placeholder label with color you want. You can download the library from IQKeyboardManager

or you can install it from cocoapods.

12

Here's a way easier solution that behaves exactly like UITextField's placeholder but doesn't require drawing custom views, or resigning first responder.

- (void) textViewDidChange:(UITextView *)textView{

    if (textView.text.length == 0){
        textView.textColor = [UIColor lightGrayColor];
        textView.text = placeholderText;
        [textView setSelectedRange:NSMakeRange(0, 0)];
        isPlaceholder = YES;

    } else if (isPlaceholder && ![textView.text isEqualToString:placeholderText]) {
        textView.text = [textView.text substringToIndex:1];
        textView.textColor = [UIColor blackColor];
        isPlaceholder = NO;
    }

}

(the second check in the else if statement is for the case where nothing is entered and the user presses backspace)

Just set your class as a UITextViewDelegate. In viewDidLoad you should initialize like

- (void) viewDidLoad{
    // initialize placeholder text
    placeholderText = @"some placeholder";
    isPlaceholder = YES;
    self.someTextView.text = placeholderText;
    self.someTextView.textColor = [UIColor lightGrayColor];
    [self.someTextView setSelectedRange:NSMakeRange(0, 0)];

    // assign UITextViewDelegate
    self.someTextView.delegate = self;
}
aning
  • 91
  • 1
  • 3
12

this is how I did it:

UITextView2.h

#import <UIKit/UIKit.h>

@interface UITextView2 : UITextView <UITextViewDelegate> {
 NSString *placeholder;
 UIColor *placeholderColor;
}

@property(nonatomic, retain) NSString *placeholder;
@property(nonatomic, retain) UIColor *placeholderColor;

-(void)textChanged:(NSNotification*)notif;

@end

UITextView2.m

@implementation UITextView2

@synthesize placeholder, placeholderColor;

- (id)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self setPlaceholder:@""];
        [self setPlaceholderColor:[UIColor lightGrayColor]];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textChanged:) name:UITextViewTextDidChangeNotification object:nil];
    }
    return self;
}

-(void)textChanged:(NSNotification*)notif {
    if ([[self placeholder] length]==0)
        return;
    if ([[self text] length]==0) {
        [[self viewWithTag:999] setAlpha:1];
    } else {
        [[self viewWithTag:999] setAlpha:0];
    }

}

- (void)drawRect:(CGRect)rect {
    if ([[self placeholder] length]>0) {
        UILabel *l = [[UILabel alloc] initWithFrame:CGRectMake(8, 8, 0, 0)];
        [l setFont:self.font];
        [l setTextColor:self.placeholderColor];
        [l setText:self.placeholder];
        [l setAlpha:0];
        [l setTag:999];
        [self addSubview:l];
        [l sizeToFit];
        [self sendSubviewToBack:l];
        [l release];
    }
    if ([[self text] length]==0 && [[self placeholder] length]>0) {
        [[self viewWithTag:999] setAlpha:1];
    }
    [super drawRect:rect];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [super dealloc];
}


@end
Cristian Bica
  • 4,067
  • 27
  • 28
10

Sorry to add another answer, But I just pulled something like this off and this created the closest-to-UITextField kind of placeholder.

Hope this helps someone.

-(void)textViewDidChange:(UITextView *)textView{
    if(textView.textColor == [UIColor lightGrayColor]){
        textView.textColor  = [UIColor blackColor]; // look at the comment section in this answer
        textView.text       = [textView.text substringToIndex: 0];// look at the comment section in this answer
    }else if(textView.text.length == 0){
        textView.text       = @"This is some placeholder text.";
        textView.textColor  = [UIColor lightGrayColor];
        textView.selectedRange = NSMakeRange(0, 0);
    }
}

-(void)textViewDidChangeSelection:(UITextView *)textView{
    if(textView.textColor == [UIColor lightGrayColor] && (textView.selectedRange.location != 0 || textView.selectedRange.length != 0)){
        textView.selectedRange = NSMakeRange(0, 0);
    }
}
rak appdev
  • 717
  • 8
  • 21
Shai Mishali
  • 9,224
  • 4
  • 56
  • 83
  • 1
    I had to change the order of the commands in the first if statement `if(textView.textColor == [UIColor lightGrayColor]){ textView.textColor = [UIColor blackColor]; textView.text = [textView.text substringToIndex: 1];` Otherwise the first character being entered in the textview was placed at the end of the text – Flexicoder Jan 31 '14 at 11:57
8

Simple way to use this within some line of code:

Take one label up to UITextView in .nib connecting this label to your code , After it.

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text{

    if (range.location>0 || text.length!=0) {
        placeholderLabel1.hidden = YES;
    }else{
        placeholderLabel1.hidden = NO;
    }
    return YES;
}
dasdom
  • 13,975
  • 2
  • 47
  • 58
SachinVsSachin
  • 6,401
  • 3
  • 33
  • 39
7

I've modified Sam Soffes' implementation to work with iOS7:

- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];

    if (_shouldDrawPlaceholder)
    {
        UIEdgeInsets insets = self.textContainerInset;        
        CGRect placeholderRect = CGRectMake(
                insets.left + self.textContainer.lineFragmentPadding,
                insets.top,
                self.frame.size.width - insets.left - insets.right,
                self.frame.size.height - insets.top - insets.bottom);

        [_placeholderText drawWithRect:placeholderRect
                           options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine
                        attributes:self.placeholderAttributes
                           context:nil];
    }
}

- (NSDictionary *)placeholderAttributes
{
    if (_placeholderAttributes == nil)
    {
        _placeholderAttributes = @
        {
            NSFontAttributeName : self.font,
            NSForegroundColorAttributeName : self.placeholderColor
        };
    }

    return _placeholderAttributes;
}

Remember to set _placeholderAttribues = nil in methods that might change the font and other thigns that might affect them. You might also want to skip "lazy" making of the attributes dictionary if that doesn't bug you.

EDIT:

Remember to call setNeedsDisplay in a overridden version of setBounds if you like the placeholder to look good after autolayout animations and the like.

Nailer
  • 2,446
  • 1
  • 24
  • 34
7

This mimics UITextField's placeholder perfectly, where the place holder text stays until you actually type something.

private let placeholder = "Type here"

@IBOutlet weak var textView: UITextView! {
    didSet {
        textView.textColor = UIColor.lightGray
        textView.text = placeholder
        textView.selectedRange = NSRange(location: 0, length: 0)
    }
}

extension ViewController: UITextViewDelegate {

    func textViewDidChangeSelection(_ textView: UITextView) {
        // Move cursor to beginning on first tap
        if textView.text == placeholder {
            textView.selectedRange = NSRange(location: 0, length: 0)
        }
    }

    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        if textView.text == placeholder && !text.isEmpty {
            textView.text = nil
            textView.textColor = UIColor.black
            textView.selectedRange = NSRange(location: 0, length: 0)
        }
        return true
    }

    func textViewDidChange(_ textView: UITextView) {
        if textView.text.isEmpty {
            textView.textColor = UIColor.lightGray
            textView.text = placeholder
        }
    }
}
AmitP
  • 5,353
  • 4
  • 35
  • 27
  • Needed to add a few more lines to get my SwiftUI app works with UITextView SwuftUI wrapper, but it looks like the most simple solution that does not include any ObjC code, 3rd party Framework or method swizzling specifically. – vedrano Apr 27 '21 at 10:26
6

You could also create a new class TextViewWithPlaceholder as a subclass of UITextView.

(This code is kind of rough -- but I think it's on the right track.)

@interface TextViewWithPlaceholder : UITextView
{

    NSString *placeholderText;  // make a property
    UIColor *placeholderColor;  // make a property
    UIColor *normalTextColor;   // cache text color here whenever you switch to the placeholderColor
}

- (void) setTextColor: (UIColor*) color
{
   normalTextColor = color;
   [super setTextColor: color];
}

- (void) updateForTextChange
{
    if ([self.text length] == 0)
    { 
        normalTextColor = self.textColor;
        self.textColor = placeholderColor;
        self.text = placeholderText;
    }
    else
    {
        self.textColor = normalTextColor;
    }

}

In your delegate, add this:

- (void)textViewDidChange:(UITextView *)textView
{
    if ([textView respondsToSelector: @selector(updateForTextChange)])
    {
        [textView updateForTextChange];
    }

}
Amagrammer
  • 6,385
  • 3
  • 28
  • 30
  • 1
    To get the exact behavior you should paint your own placeholder by overriding drawRect: (draw placeholder only if ![self isFirstResponder] && [[self text] length] == 0), and calling setNeedsDisplay inside becomeFirstResponder and resignFirstResponder – rpetrich Aug 25 '09 at 20:26
6

This thread has had plenty of answers, but here's the version I prefer.

It extends the existing UITextView class so is easily reuseable, and it doesn't intercept the events like textViewDidChange (which might break user's code, if they were already intercepting these events elsewhere).

Using my code (shown below), you can easily add a placeholder to any of your UITextViews like this:

self.textViewComments.placeholder = @"(Enter some comments here.)";

When you set this new placeholder value, it quietly adds a UILabel on top of your UITextView, then hide/shows it as necessary:

enter image description here

Okay, to make these changes, add a "UITextViewHelper.h" file containing this code:

//  UITextViewHelper.h
//  Created by Michael Gledhill on 13/02/15.

#import <Foundation/Foundation.h>

@interface UITextView (UITextViewHelper)

@property (nonatomic, strong) NSString* placeholder;
@property (nonatomic, strong) UILabel* placeholderLabel;
@property (nonatomic, strong) NSString* textValue;

-(void)checkIfNeedToDisplayPlaceholder;

@end

...and a UITextViewHelper.m file containing this:

//  UITextViewHelper.m
//  Created by Michael Gledhill on 13/02/15.
//
//  This UITextView category allows us to easily display a PlaceHolder string in our UITextView.
//  The downside is that, your code needs to set the "textValue" rather than the "text" value to safely set the UITextView's text.
//
#import "UITextViewHelper.h"
#import <objc/runtime.h>

@implementation UITextView (UITextViewHelper)

#define UI_PLACEHOLDER_TEXT_COLOR [UIColor colorWithRed:170.0/255.0 green:170.0/255.0 blue:170.0/255.0 alpha:1.0]

@dynamic placeholder;
@dynamic placeholderLabel;
@dynamic textValue;

-(void)setTextValue:(NSString *)textValue
{
    //  Change the text of our UITextView, and check whether we need to display the placeholder.
    self.text = textValue;
    [self checkIfNeedToDisplayPlaceholder];
}
-(NSString*)textValue
{
    return self.text;
}

-(void)checkIfNeedToDisplayPlaceholder
{
    //  If our UITextView is empty, display our Placeholder label (if we have one)
    if (self.placeholderLabel == nil)
        return;

    self.placeholderLabel.hidden = (![self.text isEqualToString:@""]);
}

-(void)onTap
{
    //  When the user taps in our UITextView, we'll see if we need to remove the placeholder text.
    [self checkIfNeedToDisplayPlaceholder];

    //  Make the onscreen keyboard appear.
    [self becomeFirstResponder];
}

-(void)keyPressed:(NSNotification*)notification
{
    //  The user has just typed a character in our UITextView (or pressed the delete key).
    //  Do we need to display our Placeholder label ?
   [self checkIfNeedToDisplayPlaceholder];
}

#pragma mark - Add a "placeHolder" string to the UITextView class

NSString const *kKeyPlaceHolder = @"kKeyPlaceHolder";
-(void)setPlaceholder:(NSString *)_placeholder
{
    //  Sets our "placeholder" text string, creates a new UILabel to contain it, and modifies our UITextView to cope with
    //  showing/hiding the UILabel when needed.
    objc_setAssociatedObject(self, &kKeyPlaceHolder, (id)_placeholder, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    self.placeholderLabel = [[UILabel alloc] initWithFrame:self.frame];
    self.placeholderLabel.numberOfLines = 1;
    self.placeholderLabel.text = _placeholder;
    self.placeholderLabel.textColor = UI_PLACEHOLDER_TEXT_COLOR;
    self.placeholderLabel.backgroundColor = [UIColor clearColor];
    self.placeholderLabel.userInteractionEnabled = true;
    self.placeholderLabel.font = self.font;
    [self addSubview:self.placeholderLabel];

    [self.placeholderLabel sizeToFit];

    //  Whenever the user taps within the UITextView, we'll give the textview the focus, and hide the placeholder if necessary.
    [self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap)]];

    //  Whenever the user types something in the UITextView, we'll see if we need to hide/show the placeholder label.
    [[NSNotificationCenter defaultCenter] addObserver:self selector: @selector(keyPressed:) name:UITextViewTextDidChangeNotification object:nil];

    [self checkIfNeedToDisplayPlaceholder];
}
-(NSString*)placeholder
{
    //  Returns our "placeholder" text string
    return objc_getAssociatedObject(self, &kKeyPlaceHolder);
}

#pragma mark - Add a "UILabel" to this UITextView class

NSString const *kKeyLabel = @"kKeyLabel";
-(void)setPlaceholderLabel:(UILabel *)placeholderLabel
{
    //  Stores our new UILabel (which contains our placeholder string)
    objc_setAssociatedObject(self, &kKeyLabel, (id)placeholderLabel, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    [[NSNotificationCenter defaultCenter] addObserver:self selector: @selector(keyPressed:) name:UITextViewTextDidChangeNotification object:nil];

    [self checkIfNeedToDisplayPlaceholder];
}
-(UILabel*)placeholderLabel
{
    //  Returns our new UILabel
    return objc_getAssociatedObject(self, &kKeyLabel);
}
@end

Yup, it's a lot of code, but once you've added it to your project and included the .h file...

#import "UITextViewHelper.h"

...you can easily use placeholders in UITextViews.

There's one gotcha though.

If you do this:

self.textViewComments.placeholder = @"(Enter some comments here.)";
self.textViewComments.text = @"Ooooh, hello there";

...the placeholder will appear on top of the text. When you set the text value, none of the regular notifications gets called, so I couldn't work out how to call my function to decide whether to show/hide the placeholder.

The solution is to set the textValue rather than text:

self.textViewComments.placeholder = @"(Enter some comments here.)";
self.textViewComments.textValue = @"Ooooh, hello there";

Alternatively, you can set the text value, then call checkIfNeedToDisplayPlaceholder.

self.textViewComments.text = @"Ooooh, hello there";
[self.textViewComments checkIfNeedToDisplayPlaceholder];

I like solutions like this, as they "fill the gap" between what Apple provides us with, and what we (as developers) actually need in our apps. You write this code once, add it to your library of "helper" .m/.h files, and, over time, the SDK actually starts becoming less frustrating.

(I wrote a similar helper for adding a "clear" button to my UITextViews, another thing which annoyingly exists in UITextField but not in UITextView...)

Mike Gledhill
  • 27,846
  • 7
  • 149
  • 159
  • I like how clean this is, but I dislike how it requires a second UIView/UILabel (and doesn't really inherit the attributes/colors/font easily from the UITextView). Nice contribution though – mattsven Mar 10 '15 at 16:51
6

First take a label in .h file.

Here I take

UILabel * lbl;

Then in .m under viewDidLoad declare it

lbl = [[UILabel alloc] initWithFrame:CGRectMake(8.0, 0.0,250, 34.0)];

lbl.font=[UIFont systemFontOfSize:14.0];

[lbl setText:@"Write a message..."];

[lbl setBackgroundColor:[UIColor clearColor]];

[lbl setTextColor:[UIColor lightGrayColor]];

[textview addSubview:lbl];

textview is my TextView.

Now declare

-(void)textViewDidChange:(UITextView *)textView {

 if (![textView hasText]){

    lbl.hidden = NO;

 }
 else{
    lbl.hidden = YES;
 }

}

And your Textview placeholder is ready !

Santo
  • 1,611
  • 3
  • 17
  • 37
tania_S
  • 1,350
  • 14
  • 23
6

I recommend use pod 'UITextView+Placeholder'

pod 'UITextView+Placeholder'

on your code

#import "UITextView+Placeholder.h"

////    

UITextView *textView = [[UITextView alloc] init];
textView.placeholder = @"How are you?";
textView.placeholderColor = [UIColor lightGrayColor];
6

Based on some of the great suggestions here already, I was able to put together the following lightweight, Interface-Builder-compatible subclass of UITextView, which:

  • Includes configurable placeholder text, styled just like that of UITextField.
  • Doesn't require any additional subviews or constraints.
  • Doesn't require any delegation or other behaviour from the ViewController.
  • Doesn't require any notifications.
  • Keeps that text fully separated from any outside classes looking at the field's text property.

Improvement suggestions are welcome.

Edit 1: Updated to reset placeholder formatting if actual text is set programmatically.

Edit 2: The placeholder text color can now be retrieved programmatically.

Swift v5:

import UIKit
@IBDesignable class TextViewWithPlaceholder: UITextView {
    
    override var text: String! { // Ensures that the placeholder text is never returned as the field's text
        get {
            if showingPlaceholder {
                return "" // When showing the placeholder, there's no real text to return
            } else { return super.text }
        }
        set {
            if showingPlaceholder {
                removePlaceholderFormatting() // If the placeholder text is what's being changed, it's no longer the placeholder
            }
            super.text = newValue
        }
    }
    @IBInspectable var placeholderText: String = ""
    @IBInspectable var placeholderTextColor: UIColor = .placeholderText
    private var showingPlaceholder: Bool = true // Keeps track of whether the field is currently showing a placeholder
    
    override func didMoveToWindow() {
        super.didMoveToWindow()
        if text.isEmpty {
            showPlaceholderText() // Load up the placeholder text when first appearing, but not if coming back to a view where text was already entered
        }
    }
    
    override public func becomeFirstResponder() -> Bool {
        
        // If the current text is the placeholder, remove it
        if showingPlaceholder {
            text = nil
            removePlaceholderFormatting()
        }
        return super.becomeFirstResponder()
    }
    
    override public func resignFirstResponder() -> Bool {
        
        // If there's no text, put the placeholder back
        if text.isEmpty {
            showPlaceholderText()
        }
        return super.resignFirstResponder()
    }
    
    private func showPlaceholderText() {
        
        text = placeholderText
        showingPlaceholder = true
        textColor = placeholderTextColor
    }
    
    private func removePlaceholderFormatting() {
        
        showingPlaceholder = false
        textColor = nil // Put the text back to the default, unmodified color
    }
}
TheNeil
  • 3,321
  • 2
  • 27
  • 52
  • There is an issue when setting uitextview's text manually i.e, my TextViewWithPlaceholder.text = "some text" won't show this string as txt but as placeholder text. – AfnanAhmad Dec 06 '19 at 06:03
  • 1
    @AfnanAhmad To set the `text` programmatically, make the `showingPlaceHolder` variable public, then before setting the text, set `showingPlaceHolder` to false & set the text. It should appear then. – Ahmet Gokdayi Feb 13 '20 at 14:41
  • 2
    Thanks for the feedback @AfnanAhmad. I've now made some edits to better support programmatically-set text. Best not to make public components that are designed to be private. – TheNeil Feb 13 '20 at 23:35
6

I made my own version of the subclass of 'UITextView'. I liked Sam Soffes's idea of using the notifications, but I didn't liked the drawRect: overwrite. Seems overkill to me. I think I made a very clean implementation.

You can look at my subclass here. A demo project is also included.

gcamp
  • 14,622
  • 4
  • 54
  • 85
5
    - (void)textViewDidChange:(UITextView *)textView
{
    placeholderLabel.hidden = YES;
}

put a label over the textview.

vatti
  • 463
  • 7
  • 16
5

It is not possible to create placeholder in UITextView but you can generate effect like place holder by this.

  - (void)viewDidLoad{      
              commentTxtView.text = @"Comment";
              commentTxtView.textColor = [UIColor lightGrayColor];
              commentTxtView.delegate = self;

     }
       - (BOOL) textViewShouldBeginEditing:(UITextView *)textView
     {
         commentTxtView.text = @"";
         commentTxtView.textColor = [UIColor blackColor];
         return YES;
     }

     -(void) textViewDidChange:(UITextView *)textView
     {

    if(commentTxtView.text.length == 0){
        commentTxtView.textColor = [UIColor lightGrayColor];
        commentTxtView.text = @"Comment";
        [commentTxtView resignFirstResponder];
    }
    }

OR you can add label in textview just like

       lbl = [[UILabel alloc] initWithFrame:CGRectMake(10.0, 0.0,textView.frame.size.width - 10.0, 34.0)];


[lbl setText:kDescriptionPlaceholder];
[lbl setBackgroundColor:[UIColor clearColor]];
[lbl setTextColor:[UIColor lightGrayColor]];
textView.delegate = self;

[textView addSubview:lbl];

and set

- (void)textViewDidEndEditing:(UITextView *)theTextView
{
     if (![textView hasText]) {
     lbl.hidden = NO;
}
}

- (void) textViewDidChange:(UITextView *)textView
{
    if(![textView hasText]) {
      lbl.hidden = NO;
}
else{
    lbl.hidden = YES;
 }  
}
PJR
  • 13,052
  • 13
  • 64
  • 104
4

Here's yet another way to do it, one that reproduces the slight indentation of UITextField's placeholder:

Drag a UITextField right under the UITextView so that their top left corners are aligned. Add your placeholder text to the text field.

In viewDidLoad, add:

[tView setDelegate:self];
tView.contentInset = UIEdgeInsetsMake(-8,-8,0,0);
tView.backgroundColor = [UIColor clearColor];

Then add:

- (void)textViewDidChange:(UITextView *)textView {
    if (textView.text.length == 0) {
        textView.backgroundColor = [UIColor clearColor];            
    } else {
        textView.backgroundColor = [UIColor whiteColor];
    }
}
Justin Boo
  • 10,132
  • 8
  • 50
  • 71
Chris
  • 21
  • 1
4

Lets make it easy

Create one UILabel and place it on your text view(Give the text as Placeholder-set color gray-you can do all this in your xib) Now in you header file declare the UILabel and also the the textviewDelegate Now you can simply hide the label when you click on the textview

complete code below

header

@interface ViewController :UIViewController<UITextViewDelegate>{
 }
   @property (nonatomic,strong) IBOutlet UILabel *PlceHolder_label;
   @property (nonatomic,strong) IBOutlet UITextView *TextView;

@end

implementation

@implementation UploadFoodImageViewController
@synthesize PlceHolder_label,TextView;

  - (void)viewDidLoad
    {
       [super viewDidLoad];
    }


 - (BOOL)textViewShouldBeginEditing:(UITextView *)textView{

       if([textView isEqual:TextView]){
            [PlceHolder_label setHidden:YES];
            [self.tabScrlVw setContentOffset:CGPointMake(0,150) animated:YES];
          }
      return YES;
    }

@end

Dont forget to connect the textView and UILabel to filesowner from xib

Nithin M Keloth
  • 1,595
  • 1
  • 20
  • 37
4

Take a look at UTPlaceholderTextView.

This is a convenient subclass of UITextView that supports placeholder similiar to that of UITextField. Main peculiarities:

  • Does not use subviews
  • Does not override drawRect:
  • Placeholder could be of arbitrary length, and rendered just the same way as usual text
Granfalloner
  • 422
  • 3
  • 8
4

I read through all of these, but came up with a very short, Swift 3, solution that has worked in all of my tests. It could stand a little more generality, but the process is simple. Here's the entire thing which I call "TextViewWithPlaceholder".

import UIKit

class TextViewWithPlaceholder: UITextView {

    public var placeholder: String?
    public var placeholderColor = UIColor.lightGray

    private var placeholderLabel: UILabel?

    // Set up notification listener when created from a XIB or storyboard.
    // You can also set up init() functions if you plan on creating
    // these programmatically.
    override func awakeFromNib() {
        super.awakeFromNib()

        NotificationCenter.default.addObserver(self,
                                           selector: #selector(TextViewWithPlaceholder.textDidChangeHandler(notification:)),
                                           name: .UITextViewTextDidChange,
                                           object: self)

        placeholderLabel = UILabel()
        placeholderLabel?.alpha = 0.85
        placeholderLabel?.textColor = placeholderColor
    }

    // By using layoutSubviews, you can size and position the placeholder
    // more accurately. I chose to hard-code the size of the placeholder
    // but you can combine this with other techniques shown in previous replies.
    override func layoutSubviews() {
        super.layoutSubviews()

        placeholderLabel?.textColor = placeholderColor
        placeholderLabel?.text = placeholder

        placeholderLabel?.frame = CGRect(x: 6, y: 4, width: self.bounds.size.width-16, height: 24)

        if text.isEmpty {
            addSubview(placeholderLabel!)
            bringSubview(toFront: placeholderLabel!)
        } else {
            placeholderLabel?.removeFromSuperview()
        }
    }

    // Whenever the text changes, just trigger a new layout pass.
    func textDidChangeHandler(notification: Notification) {
        layoutSubviews()
    }
}
P. Ent
  • 1,654
  • 1
  • 12
  • 22
  • Some concerns here. You should not call `layoutSubviews()` directly. And you didn't remove the NotificationCenter observer. – Hlung Nov 27 '18 at 09:54
4

I have wrote a class in swift. You can import this class whenever required.

import UIKit

public class CustomTextView: UITextView {

private struct Constants {
    static let defaultiOSPlaceholderColor = UIColor(red: 0.0, green: 0.0, blue: 0.0980392, alpha: 0.22)
}
private let placeholderLabel: UILabel = UILabel()

private var placeholderLabelConstraints = [NSLayoutConstraint]()

@IBInspectable public var placeholder: String = "" {
    didSet {
        placeholderLabel.text = placeholder
    }
}

@IBInspectable public var placeholderColor: UIColor = CustomTextView.Constants.defaultiOSPlaceholderColor {
    didSet {
        placeholderLabel.textColor = placeholderColor
    }
}

override public var font: UIFont! {
    didSet {
        placeholderLabel.font = font
    }
}

override public var textAlignment: NSTextAlignment {
    didSet {
        placeholderLabel.textAlignment = textAlignment
    }
}

override public var text: String! {
    didSet {
        textDidChange()
    }
}

override public var attributedText: NSAttributedString! {
    didSet {
        textDidChange()
    }
}

override public var textContainerInset: UIEdgeInsets {
    didSet {
        updateConstraintsForPlaceholderLabel()
    }
}

override public init(frame: CGRect, textContainer: NSTextContainer?) {
    super.init(frame: frame, textContainer: textContainer)
    commonInit()
}

required public init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    commonInit()
}

private func commonInit() {
    NSNotificationCenter.defaultCenter().addObserver(self,
                                                     selector: #selector(textDidChange),
                                                     name: UITextViewTextDidChangeNotification,
                                                     object: nil)

    placeholderLabel.font = font
    placeholderLabel.textColor = placeholderColor
    placeholderLabel.textAlignment = textAlignment
    placeholderLabel.text = placeholder
    placeholderLabel.numberOfLines = 0
    placeholderLabel.backgroundColor = UIColor.clearColor()
    placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
    addSubview(placeholderLabel)
    updateConstraintsForPlaceholderLabel()
}

private func updateConstraintsForPlaceholderLabel() {
    var newConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-(\(textContainerInset.left + textContainer.lineFragmentPadding))-[placeholder]",
                                                                        options: [],
                                                                        metrics: nil,
                                                                        views: ["placeholder": placeholderLabel])
    newConstraints += NSLayoutConstraint.constraintsWithVisualFormat("V:|-(\(textContainerInset.top))-[placeholder]",
                                                                     options: [],
                                                                     metrics: nil,
                                                                     views: ["placeholder": placeholderLabel])
    newConstraints.append(NSLayoutConstraint(
        item: placeholderLabel,
        attribute: .Width,
        relatedBy: .Equal,
        toItem: self,
        attribute: .Width,
        multiplier: 1.0,
        constant: -(textContainerInset.left + textContainerInset.right + textContainer.lineFragmentPadding * 2.0)
        ))
    removeConstraints(placeholderLabelConstraints)
    addConstraints(newConstraints)
    placeholderLabelConstraints = newConstraints
}

@objc private func textDidChange() {
    placeholderLabel.hidden = !text.isEmpty
}

public override func layoutSubviews() {
    super.layoutSubviews()
    placeholderLabel.preferredMaxLayoutWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2.0
}

deinit {
    NSNotificationCenter.defaultCenter().removeObserver(self,
                                                        name: UITextViewTextDidChangeNotification,
                                                        object: nil)
}

}

enter image description here

Sujatha Girijala
  • 1,141
  • 8
  • 20
  • 1
    This is actually one of the cleanest solutions I was able to find and I ended up using this.. Especially taking insets into consideration and using constraints for the label is a nice touch, I haven't seen many (any?) other solutions taking care of changing bounds, fonts, RTL etc. – Mark Oct 15 '18 at 12:55
4

I have created a swift 3 version of highest ranked answer

You just need to do subclassing of UITextView.

import UIKit

 class UIPlaceHolderTextView: UITextView {


//MARK: - Properties
@IBInspectable var placeholder: String?
@IBInspectable var placeholderColor: UIColor?
var placeholderLabel: UILabel?


//MARK: - Initializers
override func awakeFromNib() {
    super.awakeFromNib()


}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)

    // Use Interface Builder User Defined Runtime Attributes to set
    // placeholder and placeholderColor in Interface Builder.
    if self.placeholder == nil {
        self.placeholder = ""
    }

    if self.placeholderColor == nil {
        self.placeholderColor = UIColor.black
    }

    NotificationCenter.default.addObserver(self, selector: #selector(textChanged(_:)), name: NSNotification.Name.UITextViewTextDidChange, object: nil)

}

func textChanged(_ notification: Notification) -> Void {
    if self.placeholder?.count == 0 {
        return
    }

    UIView.animate(withDuration: 0.25) {
        if self.text.count == 0 {
            self.viewWithTag(999)?.alpha = 1
        }
        else {
            self.viewWithTag(999)?.alpha = 0
        }
    }
}

// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
    super.draw(rect)

    if (self.placeholder?.count ?? 0) > 0 {
        if placeholderLabel == nil {
            placeholderLabel = UILabel.init()
            placeholderLabel?.lineBreakMode = .byWordWrapping
            placeholderLabel?.numberOfLines = 0
            placeholderLabel?.font = self.font
            placeholderLabel?.backgroundColor = self.backgroundColor
            placeholderLabel?.textColor = self.placeholderColor
            placeholderLabel?.alpha = 0
            placeholderLabel?.tag = 999
            self.addSubview(placeholderLabel!)

            placeholderLabel?.translatesAutoresizingMaskIntoConstraints = false
            placeholderLabel?.topAnchor.constraint(equalTo: self.topAnchor, constant: 7).isActive = true
            placeholderLabel?.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 4).isActive = true
            placeholderLabel?.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
            placeholderLabel?.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
        }

        placeholderLabel?.text = self.placeholder
        placeholderLabel?.sizeToFit()
        self.sendSubview(toBack: self.placeholderLabel!)
    }

    if self.text.count == 0 && (self.placeholder?.count ?? 0) > 0 {
        self.viewWithTag(999)?.alpha = 1
    }
 }
}
3

Here's a simple and clever way to get the perfect behavior.

Let's borrow the placeholder from UITextField.

enter image description here

  1. Set up a textField and set its text transparent.

    self.placeholderTextField = [[UITextField alloc] init];
    
    /* adjust the frame to fit it in the first line of your textView */
    self.placeholderTextField.frame = CGRectMake(0.0, 0.0, yourTextView.width, 30.0);
    
    self.placeholderTextField.textColor = [UIColor clearColor];
    self.placeholderTextField.userInteractionEnabled = NO;
    self.placeholderTextField.font = yourTextView.font;
    self.placeholderTextField.placeholder = @"sample placeholder";
    [yourTextView addSubview:self.placeholderTextField];
    
  2. Set textView's delegate and synchronize the textField and textView.

    yourTextView.delegate = self;
    

    then

    - (void)textViewDidChange:(UITextView *)textView {
    
        self.placeholderTextField.text = textView.text;
    
    }
    
  3. That's all.
Tim Chen
  • 1,364
  • 12
  • 14
  • I liked this solution at first - an innovative idea - but it caused scrolling issues and lag, and is not modular, so I switched to [SAMTextView](https://github.com/soffes/SAMTextView). – jk7 Feb 28 '20 at 20:57
3

A simpler approach is to create a secondary UITextView with all of the same attributes as the original text view except for a different textColor, with constraints to ensure they stay aligned. Then when any characters are entered into the main text view, hide the cloned text view, otherwise show the cloned text view with some text.

This can be achieved in several ways, but a relatively clean way would be to subclass UITextView and keep all of this logic within the subclass.

So, subclass UITextView and allow it to create it's place holder view lazily:

Interface file:

@interface FOOTextView : UITextView <UITextViewDelegate>

@property (nonatomic, copy) NSString *placeholderText;

- (void)checkPlaceholder;

@end

Implementation file:

#import "FOOTextView.h"

@interface FOOTextView ()

@property (nonatomic, strong) UITextView *placeholderTextView;

@end

@implementation FOOTextView

- (void)checkPlaceholder {
    // Hide the placeholder text view if we've got any text
    self.placeholderTextView.hidden = (self.text.length > 0 || self.attributedText.length > 0);
}

- (void)setPlaceholderText:(NSString *)placeholderText {
    _placeholderText = [placeholderText copy];

    // Setup the placeholder text view if we haven't already
    [self setupPlaceholderTextView];

    // Apply the placeholder text to the placeholder text view
    self.placeholderTextView.text = placeholderText;
}

- (void)setupPlaceholderTextView {
    if (!self.placeholderTextView) {

        // Setup the place holder text view, duplicating our visual setup
        self.placeholderTextView = [[UITextView alloc] initWithFrame:CGRectZero];
        self.placeholderTextView.translatesAutoresizingMaskIntoConstraints = NO;
        self.placeholderTextView.textColor = self.placeholderTextColor ? self.placeholderTextColor : [UIColor colorWithRed:199.f/255.f green:199.f/255.f blue:205.f/255.f alpha:1.f];
        self.placeholderTextView.userInteractionEnabled = NO;
        self.placeholderTextView.font = self.font;
        self.placeholderTextView.textAlignment = self.textAlignment;
        self.placeholderTextView.backgroundColor = self.backgroundColor;
        self.placeholderTextView.editable = NO;

        // Our background color must be clear for the placeholder text view to show through
        self.backgroundColor = [UIColor clearColor];

        // Insert the placeholder text view into our superview, below ourself so it shows through
        [self.superview insertSubview:self.placeholderTextView belowSubview:self];

        // Setup constraints to ensure the placeholder text view stays aligned with us
        NSLayoutConstraint *constraintCenterX = [NSLayoutConstraint constraintWithItem:self.placeholderTextView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.f constant:0.f];
        NSLayoutConstraint *constraintCenterY = [NSLayoutConstraint constraintWithItem:self.placeholderTextView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.f constant:0.f];
        NSLayoutConstraint *constraintWidth = [NSLayoutConstraint constraintWithItem:self.placeholderTextView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeWidth multiplier:1.f constant:0.f];
        NSLayoutConstraint *constraintHeight = [NSLayoutConstraint constraintWithItem:self.placeholderTextView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeHeight multiplier:1.f constant:0.f];
        NSArray *constraints = @[constraintCenterX, constraintCenterY, constraintWidth, constraintHeight];
        [self.superview addConstraints:constraints];

    }
}

- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor {
    _placeholderTextColor = placeholderTextColor;
    self.placeholderTextView.textColor = _placeholderTextColor;
}

- (void)setBackgroundColor:(UIColor *)backgroundColor {
    // We don't want a background color ourselves, instead we want our placeholder text view to have the desired background color
    [self.placeholderTextView setBackgroundColor:backgroundColor];
}

- (void)removeFromSuperview {
    // Ensure we also remove our placeholder text view
    [self.placeholderTextView removeFromSuperview];
    self.placeholderTextView = nil;
    [super removeFromSuperview];
}

#pragma mark - Text View Delegation 
- (void)textViewDidChange:(UITextView *)textView {
    [self checkPlaceholder];
}

@end

Using the above class, if you set an instance of FOOTextView's delegate to itself, everything will work out of the box:

FOOTextView *myTextView = ...
myTextView.placeholderText = @"What's on your mind?";
myTextView.placeholderTextColor = [UIColor lightGrayColor];
myTextView.delegate = myTextView;

If you'd like another object to take over as the delegate, then you just need to call the text view's checkPlaceholder method in the textViewDidChange: delegate method, eg;

FOOTextView *myTextView = ...
myTextView.placeholderText = @"What's on your mind?";
myTextView.placeholderTextColor = [UIColor lightGrayColor];
myTextView.delegate = self;
self.myTextView = myTextView;

- (void)textViewDidChange:(UITextView *)textView {
    // Call the checkPlaceholder method to update the visuals
    [self.myTextView checkPlaceholder];
}
Adam Eisfeld
  • 1,424
  • 2
  • 13
  • 25
3

I know there are already a lot of answers to this one, but I didn't really find any of them sufficient (at least in Swift). I needed the "placeholder" functionality of the UITextField in my UITextView (I wanted the exact behavior, including text display attributes, animations, etc, and didn't want to have to maintain this over time). I also wanted a solution that provided the same exact border as a UITextField (not an approximated one that looks sort of like it looks right now, but one that looks exactly like it and will always look exactly like it). So while I was not initially a fan of jamming an extra control into the mix, it seemed that in order to meet my goals I had to use an actual UITextField and let it do the work.

This solution handles positioning the placeholder and keeping the font in sync between the two controls so that the placeholder text is the exact font and position as text entered into the control (something a lot of the other solutions don't address).

// This class is necessary to support "inset" (required to position placeholder 
// appropriately in TextView)
//
class TextField: UITextField
{
    var inset: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0);

    override func textRectForBounds(bounds: CGRect) -> CGRect
    {
        return UIEdgeInsetsInsetRect(bounds, inset);
    }

    override func placeholderRectForBounds(bounds: CGRect) -> CGRect
    {
        return UIEdgeInsetsInsetRect(bounds, inset);
    }
}

// This class implements a UITextView that has a UITextField behind it, where the 
// UITextField provides the border and the placeholder text functionality (so that the
// TextView looks and works like a UITextField).
//
class TextView : UITextView, UITextViewDelegate
{
    var textField = TextField();

    required init?(coder: NSCoder)
    {
        super.init(coder: coder);
    }

    override init(frame: CGRect, textContainer: NSTextContainer?)
    {
        super.init(frame: frame, textContainer: textContainer);

        self.delegate = self;

        // Create a background TextField with clear (invisible) text and disabled
        self.textField.borderStyle = UITextBorderStyle.RoundedRect;
        self.textField.textColor = UIColor.clearColor();
        self.textField.userInteractionEnabled = false;

        // Align the background TextView to where text appears in the TextField, so
        // that any placeholder will be in the correct position.
        self.textField.contentVerticalAlignment = UIControlContentVerticalAlignment.Top;
        self.textField.inset = UIEdgeInsets(
            top: self.textContainerInset.top,
            left: self.textContainerInset.left + self.textContainer.lineFragmentPadding,
            bottom: self.textContainerInset.bottom,
            right: self.textContainerInset.right
        );

        // The background TextField should use the same font (for the placeholder)
        self.textField.font = self.font;

        self.addSubview(textField);
        self.sendSubviewToBack(textField);
    }

    convenience init()
    {
        self.init(frame: CGRectZero, textContainer: nil)
    }

    override var font: UIFont?
    {
        didSet
        {
            // Keep the font of the TextView and background textField in sync
            self.textField.font = self.font;
        }
    }

    var placeholder: String? = nil
    {
        didSet
        {
            self.textField.placeholder = self.placeholder;
        }
    }

    override func layoutSubviews()
    {
        super.layoutSubviews()
        // Do not scroll the background textView
        self.textField.frame = CGRectMake(0, self.contentOffset.y, self.frame.width, self.frame.height);
    }

    // UITextViewDelegate - Note: If you replace delegate, your delegate must call this
    func scrollViewDidScroll(scrollView: UIScrollView)
    {
        // Do not scroll the background textView
        self.textField.frame = CGRectMake(0, self.contentOffset.y, self.frame.width, self.frame.height);
    }

    // UITextViewDelegate - Note: If you replace delegate, your delegate must call this
    func textViewDidChange(textView: UITextView)
    {
        // Updating the text in the background textView will cause the placeholder to 
        // appear/disappear (including any animations of that behavior - since the
        // textView is doing this itself).
        self.textField.text = self.text;
    }
}
Amal T S
  • 3,327
  • 2
  • 24
  • 57
BobDickinson
  • 2,156
  • 21
  • 18
  • I fixed it (let me know if it works). It used to be a pain to make the initializer work, so I did not support Storyboard/XIB initialization (we don't use those in our app - we create all controls programatically). I take exception to "crashes". The intent of the fatalError was to indicate that it did not support initialization from Storyboard/XIB (though that was admittedly not super clear from the message). – BobDickinson Jun 18 '16 at 06:00
3

One more simple answer for this, using CATextLayer.

Add CATextLayer to UITextView's layer. With UITextViewDelegate methods, simply change colour of CATextLayer.

func txtViewPlaceholder() {
    let textlayer = CATextLayer()

    textlayer.frame = CGRect(x: 5, y: 5, width: 200, height: 18)
    textlayer.contentsScale = UIScreen.main.scale
    textlayer.fontSize = 12
    textlayer.alignmentMode = kCAAlignmentLeft
    textlayer.string = "Enter here"
    textlayer.isWrapped = true
    textlayer.name = "placeholder"
    textlayer.backgroundColor = UIColor.white.cgColor
    textlayer.foregroundColor = UIColor.black.cgColor

    yourTxtVw.layer.insertSublayer(textlayer, at: 0)
}

func removeAddPlaceholder(remove: Bool, textView: UITextView) {
    for layers in textView.layer.sublayers! where layers.name == "placeholder" {
        
        if remove {
            (layers as! CATextLayer).foregroundColor = UIColor.white.cgColor
        } else {
            (layers as! CATextLayer).foregroundColor = UIColor.black.cgColor
        }
        
    }
}


extension YourViewController : UITextViewDelegate {

    func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
    
        removeAddPlaceholder(remove: true, textView: textView)
    
        return true
    }

    func textViewDidEndEditing(_ textView: UITextView) {
    
        if textView.text.count <= 0 {
            removeAddPlaceholder(remove: false, textView: textView)
        }
    }

}
McDonal_11
  • 3,935
  • 6
  • 24
  • 55
3

I created an instance variable to check whether I'll show the placeholder or not:

BOOL showPlaceHolder;
UITextView * textView; // and also the textView

On viewDidLoad I set:

[self setPlaceHolder]; 

Here's what this does:

- (void)setPlaceholder
{
    textView.text = NSLocalizedString(@"Type your question here", @"placeholder");
    textView.textColor = [UIColor lightGrayColor];
    self.showPlaceHolder = YES; //we save the state so it won't disappear in case you want to re-edit it
}

I also created a button to resign the keyboard. You don't have to do this but the cool thing here is that the placeholder is shown again if nothing was entered

- (void)textViewDidBeginEditing:(UITextView *)txtView 
{
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Done" style:UIBarButtonItemStyleDone target:self action:@selector(resignKeyboard)];
    if (self.showPlaceHolder == YES) 
    {
        textView.textColor = [UIColor blackColor];
        textView.text = @"";
        self.showPlaceHolder = NO;
    }
}

- (void)resignKeyboard 
{
    [textView resignFirstResponder];
    //here if you created a button like I did to resign the keyboard, you should hide it
    if (textView.text.length == 0) {
        [self setPlaceholder];
    }       
}
Lucas
  • 6,675
  • 3
  • 25
  • 43
3

OK my ansewer is a bit different I create a small class to do it for you.

TextViewShader.m file

#import "TextViewShader.h"

@implementation TextViewShader
-(id)initWithShadedTextView:(NSString *)text textViewToShade:(UITextView *)textview {
    self = [super initWithFrame:textview.frame];
    if (self) {
        if (shadeLabel==nil)
        {
            shadeLabel= [[UILabel alloc]initWithFrame:CGRectMake(10, 0, textview.frame.size.width, 30)];


    }
    shadeLabel.text =text;// @"Enter Your Support Request";
    shadeLabel.textColor = [UIColor lightGrayColor];
    [textview setDelegate: self];
    [textview addSubview:shadeLabel];
}
return self;
}

-(void)textViewDidChange:(UITextView *)textView{
        if (textView.text.length==0)
        {
            shadeLabel.hidden=false; 
        }
        else
        {
            shadeLabel.hidden=true;
        }

}

@end

TextViewShader.h file

#import <UIKit/UIKit.h>

@interface TextViewShader : UIView<UITextViewDelegate>{
    UILabel *shadeLabel;

}
-(id)initWithShadedTextView:(NSString *)text textViewToShade:(UITextView *)textview ;
@end

this is the simple one line of code usage (dont forget to add #import "TextViewShader.h")

 TextViewShader* shader = [[TextViewShader alloc]initWithShadedTextView:@"Enter Your Support Request" textViewToShade: youruitextviewToshade];

have fun :)

Ilanchezhian
  • 17,426
  • 1
  • 53
  • 55
Kiarash
  • 1,701
  • 2
  • 16
  • 20
2
- (BOOL) textViewShouldBeginEditing:(UITextView *)textView
{
    //NSLog(@"textViewShouldBeginEditing");
    if( [tvComment.text isEqualToString:@"Comment"] && [tvComment.textColor isEqual:[UIColor lightGrayColor]] ){
        tvComment.text = @"";
        tvComment.textColor = [UIColor blackColor];
    }
    return YES;
}

- (void)keyboardWillBeHidden:(NSNotification*)aNotification{
    //NSLog(@"keyboardWillBeHidden");

    //Manage comment field placeholdertext
    if(tvComment.text.length == 0){
        tvComment.textColor = [UIColor lightGrayColor];
        tvComment.text = @"Comment";
    }
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    tvComment.textColor = [UIColor lightGrayColor];
}

TVComment is the property that holds the textView in question. This will do the trick.

Adil Malik
  • 6,279
  • 7
  • 48
  • 77
2

I've written a cleaner implementation after trying some of the proposed approaches and posted it to Github. Pull requests and issues are welcome.

Some key improvements that versus other approaches presented here:

  • Doesn't allocate a UILabel inside drawRect:. (Please don't ever do that.)
  • Doesn't compare the text view's current text to a desired placeholder to swap colors.
  • Hides the placeholder while dictation is active (like UITextField).
cbowns
  • 6,295
  • 5
  • 47
  • 64
2

In .h class

@interface RateCommentViewController : UIViewController<UITextViewDelegate>{IBoutlet UITextview *commentTxtView;}

In .m class

- (void)viewDidLoad{      
    commentTxtView.text = @"Comment";
    commentTxtView.textColor = [UIColor lightGrayColor];
    commentTxtView.delegate = self;
}

- (BOOL) textViewShouldBeginEditing:(UITextView *)textView
{
    commentTxtView.text = @"";
    commentTxtView.textColor = [UIColor blackColor];
    return YES;
}

-(void) textViewDidChange:(UITextView *)textView
{
    if(commentTxtView.text.length == 0){
        commentTxtView.textColor = [UIColor lightGrayColor];
        commentTxtView.text = @"Comment";
        [commentTxtView resignFirstResponder];
    }
}
David
  • 3,285
  • 1
  • 37
  • 54
Rahul K Rajan
  • 776
  • 10
  • 19
2
- (void)viewDidLoad {
    [super viewDidLoad];

    self.textViewEmpty = YES;

    // Text view
    self.textView = [[UITextView alloc] init];
    self.textView.translatesAutoresizingMaskIntoConstraints = NO; // For AutoLayout
    self.textView.delegate = self;
    self.textView.textColor = [UIColor grayColor];
    self.textView.text = @"Placeholder";

    // Add subview and constraints
}

#pragma mark - UITextView

- (BOOL)textViewShouldBeginEditing:(UITextView *)textView {
    if (self.isTextViewEmpty) {
        textView.textColor = [UIColor blackColor];
        textView.text = @"";
    }

    return YES;
}

- (void)textViewDidChange:(UITextView *)textView {
    if (textView.text.length > 0) {
        self.textViewEmpty = NO;
    } else {
        self.textViewEmpty = YES;
    }
}

- (BOOL)textViewShouldEndEditing:(UITextView *)textView {
    if (self.isTextViewEmpty) {
        textView.textColor = [UIColor lightGrayColor];
        textView.text = placeholderText;
    }

    return YES;
}
Sukhrob
  • 901
  • 4
  • 12
  • 34
2

Simulating Native Placeholders


A common gripe is that iOS doesn't provide a native placeholder feature for textviews. The UITextView extension below attempts to address that concern by offering the convenience one would expect from a native feature, requiring only one line of code to add a placeholder to a textview instance.

The downside of this solution is, because it daisy chains delegate calls, it is vulnerable to (unlikely) changes to the UITextViewDelegate protocol in an iOS update. Specifically, if iOS adds new protocol methods and you implement any of them in the delegate for a text view with a placeholder, those methods won't be called unless you've also updated the extension to forward those calls.

Alternatively, the Inline Placeholder answer is a rock-solid and about as simple as can be.


Usage examples:


   • If the text view gaining the placeholder doesn't use a UITextViewDelegate:

    /* Swift 3 */

    class NoteViewController : UIViewController {
        @IBOutlet weak var noteView: UITextView!
        override func viewDidLoad() {
            noteView.addPlaceholder("Enter some text...",  color: UIColor.lightGray)
        }
    }

                                            -- OR --

   • If the text view gaining the placeholder does use a UITextViewDelegate:

    /* Swift 3 */

    class NoteViewController : UIViewController, UITextViewDelegate {
        @IBOutlet weak var noteView: UITextView!
        override func viewDidLoad() {
            noteView.addPlaceholder("Phone #", color: UIColor.lightGray, delegate: self)
        }
    }

Implementation (UITextView extension):


/* Swift 3 */

extension UITextView: UITextViewDelegate
{

    func addPlaceholder(_ placeholderText : String, 
                      color : UIColor? = UIColor.lightGray,
                      delegate : UITextViewDelegate? = nil) {

        self.delegate = self             // Make receiving textview instance a delegate
        let placeholder = UITextView()   // Need delegate storage ULabel doesn't provide
        placeholder.isUserInteractionEnabled = false  //... so we *simulate* UILabel
        self.addSubview(placeholder)     // Add to text view instance's view tree               
        placeholder.sizeToFit()          // Constrain to fit inside parent text view
        placeholder.tintColor = UIColor.clear // Unused in textviews. Can host our 'tag'
        placeholder.frame.origin = CGPoint(x: 5, y: 0) // Don't cover I-beam cursor
        placeholder.delegate = delegate  // Use as cache for caller's delegate 
        placeholder.font = UIFont.italicSystemFont(ofSize: (self.font?.pointSize)!)
        placeholder.text = placeholderText
        placeholder.textColor = color
    }

      
    func findPlaceholder() -> UITextView? { // find placeholder by its tag 
        for subview in self.subviews {
            if let textview = subview as? UITextView {
                if textview.tintColor == UIColor.clear { // sneaky tagging scheme
                    return textview
                }
            }
        }
        return nil
    }
     
    /* 
     * Safely daisychain to caller delegate methods as appropriate...
     */

    public func textViewDidChange(_ textView: UITextView) { // ←  need this delegate method
        if let placeholder = findPlaceholder() {
            placeholder.isHidden = !self.text.isEmpty // ← ... to do this
            placeholder.delegate?.textViewDidChange?(textView)
        } 
    }

    /* 
     * Since we're becoming a delegate on behalf of this placeholder-enabled
     * text view instance, we must forward *all* that protocol's activity expected
     * by the instance, not just the particular optional protocol method we need to
     * intercept, above.
     */

    public func textViewDidEndEditing(_ textView: UITextView) {
        if let placeholder = findPlaceholder() {
            placeholder.delegate?.textViewDidEndEditing?(textView)
        } 
    }

    public func textViewDidBeginEditing(_ textView: UITextView) {
        if let placeholder = findPlaceholder() {
            placeholder.delegate?.textViewDidBeginEditing?(textView)
        } 
    }

    public  func textViewDidChangeSelection(_ textView: UITextView) {
        if let placeholder = findPlaceholder() {
            placeholder.delegate?.textViewDidChangeSelection?(textView)
        } 
    }

    public func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
        if let placeholder = findPlaceholder() {
            guard let retval = placeholder.delegate?.textViewShouldEndEditing?(textView) else {
                return true
            }
            return retval
        }
        return true
    }

    public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
        if let placeholder = findPlaceholder() {
            guard let retval = placeholder.delegate?.textViewShouldBeginEditing?(textView) else {
                return true
            }
            return retval
        } 
        return true
    }

    public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        if let placeholder = findPlaceholder() {
            guard let retval = placeholder.delegate?.textView?(textView, shouldChangeTextIn: range, replacementText: text) else {
                return true
            }
            return retval
        } 
        return true
    }

    public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        if let placeholder = findPlaceholder() {
                guard let retval = placeholder.delegate?.textView?(textView, shouldInteractWith: URL, in: characterRange, interaction:
                    interaction) else {
                        return true
            }
            return retval
        }
        return true
    }

    public func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        if let placeholder = findPlaceholder() {
            guard let retval = placeholder.delegate?.textView?(textView, shouldInteractWith: textAttachment, in: characterRange, interaction: interaction) else {
                return true
            }
            return retval
        }
        return true
    }
}

1. As an extension of an essential iOS class like UITextView, it's important to know that this code has no interaction with any textviews that don't activate a placeholder, e.g textview instances that haven't been initialized with a call addPlaceholder()

2. Placeholder-enabled text views transparently become a UITextViewDelegate to track character count, in order to control placeholder visibility. If a delegate is passed to addPlaceholder(), this code daisy-chains (i.e. forwards) delegate callbacks to that delegate.

3. The author is investigating ways to inspect the UITextViewDelegate protocol and proxy it automatically without having to hardcode each method. That would inoculate the code from method signature changes and new methods being added to the protocol.

Community
  • 1
  • 1
clearlight
  • 12,255
  • 11
  • 57
  • 75
2

Another solution

import UIKit

protocol PlaceholderTextViewDelegate: class {

    func placeholderTextViewDidChangeText(_ text: String)
    func placeholderTextViewDidEndEditing(_ text: String)
}

final class PlaceholderTextView: UITextView {

    weak var notifier: PlaceholderTextViewDelegate?
    var ignoreEnterAction: Bool = true

    var placeholder: String? {
        didSet {
            text = placeholder
            selectedRange = NSRange(location: 0, length: 0)
        }
    }

    var placeholderColor = UIColor.lightGray {
        didSet {
            if text == placeholder {
                textColor = placeholderColor
            }
        }
    }
    var normalTextColor = UIColor.lightGray

    var placeholderFont = UIFont.sfProRegular(28)

    fileprivate var placeholderLabel: UILabel?

    // MARK: - LifeCycle

    override var text: String? {
        didSet {
            if text == placeholder {
                textColor = placeholderColor
            } else {
                textColor = normalTextColor
            }
        }
    }

    init() {
        super.init(frame: CGRect.zero, textContainer: nil)
        awakeFromNib()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func awakeFromNib() {
        super.awakeFromNib()

        self.delegate = self
    }
}

extension PlaceholderTextView: UITextViewDelegate {

    // MARK: - UITextViewDelegate
    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {

        if text == "" && textView.text == placeholder {
            return false
        }

        if let placeholder = placeholder,
            textView.text == placeholder,
            range.location <= placeholder.count {
            textView.text = ""
        }

        if ignoreEnterAction && text == "\n" {
            textView.resignFirstResponder()
            return false
        }
        return true
    }

    func textViewDidChange(_ textView: UITextView) {
        if let placeholder = placeholder {
            textView.text = textView.text.replacingOccurrences(of: placeholder, with: "")
        }

        if let placeholder = placeholder,
            text?.isEmpty == true {
            text = placeholder
            textColor = placeholderColor

            selectedRange = NSRange(location: 0, length: 0)
        } else {
            textColor = normalTextColor
        }

        notifier?.placeholderTextViewDidChangeText(textView.text)
    }

    func textViewDidChangeSelection(_ textView: UITextView) {
        if let placeholder = placeholder,
            text == placeholder {
            selectedRange = NSRange(location: 0, length: 0)
        }
    }

    func textViewDidEndEditing(_ textView: UITextView) {
        notifier?.placeholderTextViewDidEndEditing(textView.text)

        if let placeholder = placeholder,
            text?.isEmpty == true {
            text = placeholder
            textColor = placeholderColor
            selectedRange = NSRange(location: 0, length: 0)
        } else {
            textColor = normalTextColor
        }
    }
}

result:

enter image description here

hbk
  • 10,908
  • 11
  • 91
  • 124
2

TextView PlaceHolder In swift

import UIKit

@IBDesignable
open class KMPlaceholderTextView: UITextView {

    private struct Constants {
        static let defaultiOSPlaceholderColor = UIColor(red: 0.0, green: 0.0, blue: 0.0980392, alpha: 0.22)
    }

    public let placeholderLabel: UILabel = UILabel()

    private var placeholderLabelConstraints = [NSLayoutConstraint]()

    @IBInspectable open var placeholder: String = "" {
        didSet {
            placeholderLabel.text = placeholder
        }
    }

    @IBInspectable open var placeholderColor: UIColor = KMPlaceholderTextView.Constants.defaultiOSPlaceholderColor {
        didSet {
            placeholderLabel.textColor = placeholderColor
        }
    }

    override open var font: UIFont! {
        didSet {
            if placeholderFont == nil {
                placeholderLabel.font = font
            }
        }
    }

    open var placeholderFont: UIFont? {
        didSet {
            let font = (placeholderFont != nil) ? placeholderFont : self.font
            placeholderLabel.font = font
        }
    }

    override open var textAlignment: NSTextAlignment {
        didSet {
            placeholderLabel.textAlignment = textAlignment
        }
    }

    override open var text: String! {
        didSet {
            textDidChange()
        }
    }

    override open var attributedText: NSAttributedString! {
        didSet {
            textDidChange()
        }
    }

    override open var textContainerInset: UIEdgeInsets {
        didSet {
            updateConstraintsForPlaceholderLabel()
        }
    }

    override public init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        commonInit()
    }

    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }

    private func commonInit() {
        #if swift(>=4.2)
        let notificationName = UITextView.textDidChangeNotification
        #else
        let notificationName = NSNotification.Name.UITextView.textDidChangeNotification
        #endif

        NotificationCenter.default.addObserver(self,
                                               selector: #selector(textDidChange),
                                               name: notificationName,
                                               object: nil)

        placeholderLabel.font = font
        placeholderLabel.textColor = placeholderColor
        placeholderLabel.textAlignment = textAlignment
        placeholderLabel.text = placeholder
        placeholderLabel.numberOfLines = 0
        placeholderLabel.backgroundColor = UIColor.clear
        placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
        addSubview(placeholderLabel)
        updateConstraintsForPlaceholderLabel()
    }

    private func updateConstraintsForPlaceholderLabel() {
        var newConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(\(textContainerInset.left + textContainer.lineFragmentPadding))-[placeholder]",
            options: [],
            metrics: nil,
            views: ["placeholder": placeholderLabel])
        newConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-(\(textContainerInset.top))-[placeholder]",
            options: [],
            metrics: nil,
            views: ["placeholder": placeholderLabel])
        newConstraints.append(NSLayoutConstraint(
            item: placeholderLabel,
            attribute: .width,
            relatedBy: .equal,
            toItem: self,
            attribute: .width,
            multiplier: 1.0,
            constant: -(textContainerInset.left + textContainerInset.right + textContainer.lineFragmentPadding * 2.0)
        ))
        removeConstraints(placeholderLabelConstraints)
        addConstraints(newConstraints)
        placeholderLabelConstraints = newConstraints
    }

    @objc private func textDidChange() {
        placeholderLabel.isHidden = !text.isEmpty
        self.layoutIfNeeded()
    }

    open override func layoutSubviews() {
        super.layoutSubviews()
        placeholderLabel.preferredMaxLayoutWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2.0
    }

    deinit {
        #if swift(>=4.2)
        let notificationName = UITextView.textDidChangeNotification
        #else
        let notificationName = NSNotification.Name.UITextView.textDidChangeNotification
        #endif

        NotificationCenter.default.removeObserver(self,
                                                  name: notificationName,
                                                  object: nil)
    }

}

Usage

enter image description here

1

If your looking for a simple way to achieve this , try my approach:

- (BOOL)textViewShouldBeginEditing:(UITextView *)textView
{
    if ([[textView text] isEqualToString:PLACE_HOLDER_TEXT]) {
          textView.text = @"";
          textView.textColor = [UIColor blackColor];
    }

    return YES;
}

-(BOOL)textViewShouldEndEditing:(UITextView *)textView
{
    if ([[textView text] length] == 0) {
        textView.text = PLACE_HOLDER_TEXT;
        textView.textColor = [UIColor lightGrayColor];
    }
    return YES;
}

Yes, that's it PLACE_HOLDER_TEXT is an NSString containing your placeholder

meda
  • 45,103
  • 14
  • 92
  • 122
1

I found own solution

- (void)textViewDidBeginEditing:(UITextView *)textView
{
    if ([textView.text isEqualToString:PLACEHOLDER_TEXT])
    {
        textView.textColor = [UIColor lightGrayColor];
        dispatch_async(dispatch_get_main_queue(), ^
                       {
                           textView.selectedRange = NSMakeRange(0, 0);
                       });
    }
    else
    {
        textView.textColor = [UIColor blackColor];
    }

    [textView becomeFirstResponder];
}

- (void)textViewDidEndEditing:(UITextView *)textView
{
    if ([textView.text isEqualToString:@""])
    {
        textView.text = PLACEHOLDER_TEXT;
        textView.textColor = [UIColor lightGrayColor];
    }

    [textView resignFirstResponder];
}

- (BOOL)textView:(UITextView *)textView
shouldChangeTextInRange:(NSRange)range
 replacementText:(NSString *)text
{
    if (range.location == 0 && range.length == [[textView text] length] && [text isEqualToString:@""])
    {
        textView.text = PLACEHOLDER_TEXT;
        textView.textColor = [UIColor lightGrayColor];

        dispatch_async(dispatch_get_main_queue(), ^
                       {
                           textView.selectedRange = NSMakeRange(0, 0);
                       });

        return NO;
    }

    if ([textView.text isEqualToString:PLACEHOLDER_TEXT])
    {
        textView.text = @"";
        textView.textColor = [UIColor blackColor];
    }

    return YES;
}
Roman
  • 366
  • 5
  • 19
1

Yet another answer:

https://github.com/gcamp/GCPlaceholderTextView

Change Class of UITextView in IB to GCPlaceholderTextView and set the placeholder property

gndp
  • 231
  • 2
  • 12
1

Swift 3.1

After trying all swift answers, this answer would have saved me 3 hours of research. Hope this helps.

  1. Make sure your textField (no matter the custom name you have) is pointing to its delegate in Storyboard and has an @IBOutlet with yourCustomTextField

  2. Add to viewDidLoad() the following, it will appear when loading the view:

Show me what looks to be a placeholder:

yourCustomTextField = "Start typing..." 
yourCustomTextField.textColor = .lightGray
  1. Outside the viewDidLoad but inside the same class add the following declarations: UIViewController, UITextViewDelegate, UINavigationControllerDelegate

This code will make yourCustomTextField go away when typing in the textField:

func textViewDidBeginEditing (_ textView: UITextView) { 

    if (textView.text == "Start typing...") {

        textView.text = ""
        textView.textColor = .black
    }

    textView.becomeFirstResponder()
}

func textViewDidEndEditing(_ textView: UITextView) {
    if (textView.text == "") {

        textView.text = "Start typing..."
        textView.textColor = .lightGray
    }

    textView.resignFirstResponder()
}
Victor
  • 504
  • 3
  • 9
1

Here is code for swift 3.1

Original code by Jason George in first answer.

Don't forget to set your custom class for TextView in interface builder to UIPlaceHolderTextView and then set placeholder and placeHolder properties.

import UIKit

@IBDesignable
class UIPlaceHolderTextView: UITextView {

@IBInspectable var placeholder: String = ""
@IBInspectable var placeholderColor: UIColor = UIColor.lightGray

private let uiPlaceholderTextChangedAnimationDuration: Double = 0.05
private let defaultTagValue = 999

private var placeHolderLabel: UILabel?

override func awakeFromNib() {
    super.awakeFromNib()
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(textChanged),
        name: NSNotification.Name.UITextViewTextDidChange,
        object: nil
    )
}

override init(frame: CGRect, textContainer: NSTextContainer?) {
    super.init(frame: frame, textContainer: textContainer)
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(textChanged),
        name: NSNotification.Name.UITextViewTextDidChange,
        object: nil
    )
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(textChanged),
        name: NSNotification.Name.UITextViewTextDidChange,
        object: nil
    )
}

deinit {
    NotificationCenter.default.removeObserver(
        self,
        name: NSNotification.Name.UITextViewTextDidChange,
        object: nil
    )
}

@objc private func textChanged() {
    guard !placeholder.isEmpty else {
        return
    }
    UIView.animate(withDuration: uiPlaceholderTextChangedAnimationDuration) {
        if self.text.isEmpty {
            self.viewWithTag(self.defaultTagValue)?.alpha = CGFloat(1.0)
        }
        else {
            self.viewWithTag(self.defaultTagValue)?.alpha = CGFloat(0.0)
        }
    }
}

override var text: String! {
    didSet{
        super.text = text
        textChanged()
    }
}

override func draw(_ rect: CGRect) {
    if !placeholder.isEmpty {
        if placeHolderLabel == nil {
            placeHolderLabel = UILabel.init(frame: CGRect(x: 0, y: 8, width: bounds.size.width - 16, height: 0))
            placeHolderLabel!.lineBreakMode = .byWordWrapping
            placeHolderLabel!.numberOfLines = 0
            placeHolderLabel!.font = font
            placeHolderLabel!.backgroundColor = UIColor.clear
            placeHolderLabel!.textColor = placeholderColor
            placeHolderLabel!.alpha = 0
            placeHolderLabel!.tag = defaultTagValue
            self.addSubview(placeHolderLabel!)
        }

        placeHolderLabel!.text = placeholder
        placeHolderLabel!.sizeToFit()
        self.sendSubview(toBack: placeHolderLabel!)

        if text.isEmpty && !placeholder.isEmpty {
            viewWithTag(defaultTagValue)?.alpha = 1.0
        }
    }

    super.draw(rect)
}
}
Oleg
  • 132
  • 1
  • 4
1

I followed the code from this link. There are only 7 easy steps. It adds a UILabel to the textView and hides/shows the label when text is entered or removed from the textView via the textView's textViewDidChangeSelection(_ textView: UITextView) delegate method. I put the steps in the comments above the code.

// 1. make sure to include the UITextViewDelegate
class YourClass: UITextViewDelegate {

    @IBOutlet weak var textView : UITextView!

    // 2. create placeholder textLabel
    let placeHolderTextLabel: UILabel = {
        let placeholderLabel = UILabel()
        placeholderLabel.text = "Placeholder text..."
        placeholderLabel.sizeToFit()
        placeholderLabel.textColor = UIColor.lightGray
        return placeholderLabel
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 3. set textView delegate
        textView.delegate = self

        configurePlaceholderTextLabel()
    }


    func configurePlaceholderTextLabel() {

        // 4. add placeholder label to textView, set it's frame and font
        textView.addSubview(placeHolderTextLabel)
        placeHolderTextLabel.frame.origin = CGPoint(x: 5, y: (textView.font?.pointSize)! / 2)
        placeHolderTextLabel.font = UIFont.systemFont(ofSize: (textView.font?.pointSize)!)

        // 5. decide wether the placeHolderTextLabel is hidden or not depending on if there is or isn't text inside the textView
        placeHolderTextLabel.isHidden = !textView.text.isEmpty

    }

    // 6. implement textView delegate method to update the placeHolderTextLabel when the text is changed
    func textViewDidChangeSelection(_ textView: UITextView) {

        // 7. decide wether the placeHolderTextLabel is hidden or not depending on if there is or isn't text inside the textView when text in textView is changed
        placeHolderTextLabel.isHidden = !textView.text.isEmpty
    }

}
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256
0

Simpler and still accounting for some text entered by the user at some point

BOOL placeHolderTextVisible;

on viewDidLoad, set it to YES (or DidMoveToSuperview, or awakeFromNib)

then, on - (BOOL) textView:(UITextView*)textView shouldBeginEditing

    - (BOOL)textViewShouldBeginEditing:(UITextView *)textView;
{
   if (placeHolderTextVisible) {
    placeHolderTextVisible = NO;
    textView.text = @"";
   }
 return YES;
}
Stephen J
  • 2,367
  • 2
  • 25
  • 31
0

You can just set a label on the textview.

MyUITextView.h

@interface MyUITextView : UITextView {
    UILabel* _placeholderLabel;
}

@property(nonatomic, assign)NSString *placeholder;

MyUITextView.m

@implementation MyUITextView

- (id)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        // Create placeholder
        viewFrame = CGRectMake(0, 0, frame.size.width, 15);
        _placeholderLabel = [[UILabel alloc] initWithFrame:viewFrame];
        _placeholderLabel.textColor = [UIColor lightGrayColor];
        [self addSubview:_placeholderLabel];

        // Add text changed notification 
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textChanged:) name:UITextViewTextDidChangeNotification object:nil];
    }
    return self;
}

- (void)setPlaceholder:(NSString *)placeholder {
    _placeholderLabel.text = placeholder;
}

- (NSString*)placeholder {
    return _placeholderLabel.text;
}

#pragma mark UITextViewTextDidChangeNotification

- (void)textChanged:(NSNotification *)notification {
    _placeholderLabel.hidden = ([self.text lenght] == 0);
}

@end
Bhavin Bhadani
  • 22,224
  • 10
  • 78
  • 108
evya
  • 3,381
  • 1
  • 25
  • 28
0

Jason's answer will looks a little bit off in iOS7, fix it by adjust the offset of _placeHolderLabel:

- (void)drawRect:(CGRect)rect
{
    if( [[self placeholder] length] > 0 )
    {
        if (_placeHolderLabel == nil )
        {
            if ([[UIDevice currentDevice].systemVersion floatValue] >= 7)
                _placeHolderLabel = [[UILabel alloc] initWithFrame:CGRectMake(4,8,self.bounds.size.width - 8,0)];
            else
                _placeHolderLabel = [[UILabel alloc] initWithFrame:CGRectMake(8,8,self.bounds.size.width - 16,0)];
            _placeHolderLabel.lineBreakMode = NSLineBreakByWordWrapping;
            _placeHolderLabel.numberOfLines = 0;
            _placeHolderLabel.font = self.font;
            _placeHolderLabel.backgroundColor = [UIColor clearColor];
            _placeHolderLabel.textColor = self.placeholderColor;
            _placeHolderLabel.alpha = 0;
            _placeHolderLabel.tag = 999;
            [self addSubview:_placeHolderLabel];
        }

        _placeHolderLabel.text = self.placeholder;
        [_placeHolderLabel sizeToFit];
        [self sendSubviewToBack:_placeHolderLabel];
    }

    if( [[self text] length] == 0 && [[self placeholder] length] > 0 )
    {
        [[self viewWithTag:999] setAlpha:1];
    }

    [super drawRect:rect];
}
Community
  • 1
  • 1
Perisheroy
  • 239
  • 4
  • 8
0

I've just found that as of iOS 10 you can now actually cast an UITextView to a method as an UITextField and set inside the method the placeholder. Just tried and it works without having to subclass UITextView.

This is an example of what worked for me:

-(void)customizeTextField:(UITextField *)textField placeholder:(NSString *)pText withColor:(UIColor *)pTextColor{

        textField.attributedPlaceholder = [[NSAttributedString alloc]
                                          initWithString:pText
                                          attributes:@{NSForegroundColorAttributeName:pTextColor}];
    }

And to use it for a UITextView you just have to pass it to the method using a cast like this:

[self customizeTextField:(UITextField*)_myTextView placeholder:@"Placeholder" withColor:[UIColor blackColor]];

N.B: After a test I've found out that the solution works fine also on iOS9.x but causes a crash on iOS8.x

LS_
  • 6,763
  • 9
  • 52
  • 88
0

After looking through (and trying out) most of the proposed solutions to this seemingly obvious - but missing - feature of UITextView, the 'best' closest I found was that from BobDickinson. But I didnt like having to resort to a whole new subclass [I prefer drop-in categories for such simple functional additions], nor that it intercepted UITextViewDelegate methods, which is probably going to mess up your existing UITextView handling code. So here's my take on a drop-in category that'll work on any existing UITextView instance...

#import <objc/runtime.h>

// Private subclass needed to override placeholderRectForBounds: to correctly position placeholder
@interface _TextField : UITextField
@property UIEdgeInsets insets;
@end
@implementation _TextField
- (CGRect)placeholderRectForBounds:(CGRect)bounds
{
    CGRect rect = [super placeholderRectForBounds:bounds];
    return UIEdgeInsetsInsetRect(rect, _insets);
}
@end

@implementation UITextView (Placeholder)

static const void *KEY;

- (void)setPlaceholder:(NSString *)placeholder
{
    _TextField *textField = objc_getAssociatedObject(self, &KEY);
    if (!textField) {
        textField = [_TextField.alloc initWithFrame:self.bounds];
        textField.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        textField.userInteractionEnabled = NO;
        textField.font = self.font;

        textField.contentVerticalAlignment = UIControlContentVerticalAlignmentTop;
        textField.insets = UIEdgeInsetsMake(self.textContainerInset.top,
                                            self.textContainerInset.left + self.textContainer.lineFragmentPadding,
                                            self.textContainerInset.bottom,
                                            self.textContainerInset.right);
        [self addSubview:textField];
        [self sendSubviewToBack:textField];

        objc_setAssociatedObject(self, &KEY, textField, OBJC_ASSOCIATION_RETAIN);

        [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(updatePlaceholder:) name:UITextViewTextDidChangeNotification object:nil];
    }
    textField.placeholder = placeholder;
}

- (NSString*)placeholder
{
    UITextField *textField = objc_getAssociatedObject(self, &KEY);
    return textField.placeholder;
}

- (void)updatePlaceholder:(NSNotification *)notification
{
    UITextField *textField = objc_getAssociatedObject(self, &KEY);
    textField.font = self.font;
    [textField setAlpha:self.text.length? 0 : 1];
}

@end

Its simple to use, just the obvious

UITextView *myTextView = UITextView.new;
...
myTextView.placeholder = @"enter text here";

It works by adding a UITextField - in the right location - behind your UITextView, and exploiting it's placeholder instead (hence you dont have to worry about getting the color right, etc), then listening for notifications whenever your UITextView is changed to show/hide this UITextField (and hence it doesnt interfere with your existing UITextViewDelegate calls). And there's no magic numbers involved... :-)

The objc_setAssociatedObject()/objc_getAssociatedObject() is to avoid having to subclass UITextView. [Unfortunately, to position the UITextField correctly, it was necessary to introduce a 'private' subclass, to override placeholderRectForBounds:]

Adapted from BobDickinson's Swift answer.

tiritea
  • 1,229
  • 13
  • 18
0

Simply create @IBDesignable subclass of your UITextView:

@IBDesignable class AttributedTextView: UITextView {

    private let placeholderLabel = UILabel()

    @IBInspectable var placeholder: String = "" {

        didSet {

            setupPlaceholderLabelIfNeeded()
            textViewDidChange()
        }
    }

    override var text: String! {

        didSet {
            textViewDidChange()
        }
    }

    //MARK: - Initialization

    override func awakeFromNib() {
        super.awakeFromNib()

        setupPlaceholderLabelIfNeeded()
        NotificationCenter.default.addObserver(self, selector: #selector(textViewDidChange), name: .UITextViewTextDidChange, object: nil)
    }

    //MARK: - Deinitialization

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    //MARK: - Internal

    func textViewDidChange() {

        placeholderLabel.isHidden = !text.isEmpty
        layoutIfNeeded()
    }

    //MARK: - Private

    private func setupPlaceholderLabelIfNeeded() {

        placeholderLabel.removeFromSuperview()
        placeholderLabel.frame = CGRect(x: 0, y: 8, width: frame.size.width, height: 0)
        placeholderLabel.textColor = UIColor.lightGray
        placeholderLabel.text = placeholder

        placeholderLabel.sizeToFit()

        insertSubview(placeholderLabel, at: 0)
    }
}

and then simply setup your placeholder in identity inspector:

enter image description here

Bartłomiej Semańczyk
  • 59,234
  • 49
  • 233
  • 358
0

This is my version of UITextView with placeholder support. Swift 4.2 https://gist.github.com/hlung/c5dda3a0c2087e5ae6c1fce8822c4713

A UITextView subclass with placeholder text support. It uses another UILabel to show the placeholder, shown when text is empty.

Hlung
  • 13,850
  • 6
  • 71
  • 90
0

In swift 5. Works fine.

class BaseTextView: UITextView {

    // MARK: - Views
    private var placeholderLabel: UIlabel!

    // MARK: - Init
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        setupUI()
        startupSetup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupUI()
        startupSetup()
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }    
}

// MARK: - Setup UI
private extension BaseTextView {
    func setupUI() {
        addPlaceholderLabel()

        textColor = .textColor
    }

    func addPlaceholderLabel() {
        placeholderLabel = BaseLabel(frame: .zero)
        placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
        insertSubview(placeholderLabel, at: 0)

        placeholderLabel.alpha = 0
        placeholderLabel.numberOfLines = 0
        placeholderLabel.backgroundColor = .clear
        placeholderLabel.textColor = .lightTextColor
        placeholderLabel.lineBreakMode = .byWordWrapping
        placeholderLabel.isUserInteractionEnabled = false
        placeholderLabel.font = UIFont.openSansSemibold.withSize(12)

        placeholderLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8).isActive = true
        placeholderLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 5).isActive = true
        placeholderLabel.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor, constant: -8).isActive = true
        placeholderLabel.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8).isActive = true
    }
}

// MARK: - Startup
private extension BaseTextView {
    func startupSetup() {
        addObservers()
        textChanged(nil)
        font = UIFont.openSansSemibold.withSize(12)
    }    

    func addObservers() {
        NotificationCenter.default.addObserver(self, selector: #selector(textChanged(_:)), name: UITextView.textDidChangeNotification, object: nil)
    }
}

// MARK: - Actions
private extension BaseTextView {
    @objc func textChanged(_ sender: Notification?) {
        UIView.animate(withDuration: 0.2) {
            self.placeholderLabel.alpha = self.text.count == 0 ? 1 : 0
        }    
    }
}

// MARK: - Public methods
extension BaseTextView {
    public func setPlaceholder(_ placeholder: String) {
        placeholderLabel.text = placeholder
    }
}
Ruben Nahatakyan
  • 372
  • 3
  • 13
0

After going through all answers, I realised I need something simple, and at the same time reusable so that I can add the same functionality to all UITextViews in my project. And I ended up with the following code:

extension UITextView {

// MARK: TextView PlaceHolderLabel Setup

func createPlaceHolderLabel(with text: String) {
    let lbl = UILabel()
    self.addSubview(lbl)

// Add your constraints here

    lbl.text = text
    lbl.textColor = .lightGray
}

// My Textview contains only one UILabel, and for my use case the below code works, tweak it according to your use case

// Lastly two methods to toggle between show and hide the placeholder label

func hidePlaceHolderLabel() {
    guard let lbl = self.subviews.first(where: { $0 is UILabel }) else { return }
    lbl.isHidden = true
}

func showPlaceHolderLabel() {
    guard let lbl = self.subviews.first(where: { $0 is UILabel }) else { return }
    lbl.isHidden = false
}

}

When UILabel is added as subView to UITextView, textView cursor position and UILabel position will mismatch, for that reason leave one space before your placeholder text

class ViewController: UIViewController, UITextViewDelegate {
    var textView: UITextView!

override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(textView)
    // Also setup textView constraints as per your need
    // Add placeholder to your textView
    // Leave one space before placeholder string
    textView.createPlaceHolderLabel(with: " Address")
    textView.delegate = self
}

Then in textViewdidChange method add the following code

func textViewDidChange(_ textView: UITextView) {
    if textView.text.isEmpty {
        textView.showPlaceHolderLabel()
    } else {
        textView.hidePlaceHolderLabel()
    }
}

It can be reused across all UITextViews.

cigien
  • 57,834
  • 11
  • 73
  • 112
theNoobDev10
  • 409
  • 5
  • 6
-1

Simple class to support icon attribted placeholders in UITextView PlaceholderTextView

@IBOutlet weak var tvMessage: PlaceholderTextView!
//  TODO: - Create Icon Text Attachment
let icon: NSTextAttachment = NSTextAttachment()
icon.image = UIImage(named: "paper-plane")
let iconString = NSMutableAttributedString(attributedString: NSAttributedString(attachment: icon))

tvMessage.icon = icon

//  TODO: - Attributes
let textColor = UIColor.gray
let lightFont = UIFont(name: "Helvetica-Light", size: tvMessage.font!.pointSize)
let italicFont = UIFont(name: "Helvetica-LightOblique", size: tvMessage.font!.pointSize)

//  TODO: - Placeholder Attributed String
let message = NSAttributedString(string: " " + "Personal Message", attributes: [ NSFontAttributeName: lightFont!,   NSForegroundColorAttributeName: textColor])
iconString.append(message)
// TODO: - Italic Placeholder Part
let option = NSAttributedString(string: " " + "Optional", attributes: [ NSFontAttributeName: italicFont!, NSForegroundColorAttributeName: textColor])
iconString.append(option)

tvMessage.attributedPlaceHolder = iconString

tvMessage.layoutSubviews()

Empty With text

LLIAJLbHOu
  • 1,313
  • 12
  • 17
-1

The easiest method to modify the placeholder text color is through the XCode storyboard interface builder. Select the UITextField of interest and open the identity inspector on the right. Click on the plus symbol in the User Defined Runtime Attributes and add a new row with Key Path as _placeholderLabel.textColor, Type as Color and Value to your desired color.

-4

I was able to do add a "place holder" to a UITextView with ALOT less code. This is what I did:

UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(60, 800, 200, 60)];
textView.text = @"Write characters here...";
textView.textColor=[UIColor grayColor];
textView.font = [UIFont fontWithName:@"Hevlatica" size:15];
textView.delegate=self;

I guess it's not an actual placeholder because you have to delete the text before you write but it could help if you wanted something a bit more simple.

Andrea F
  • 733
  • 2
  • 9
  • 16
  • I think this answer is unfairly downvoted. This is a really neat and clever solution. Plus, overriding drawRect is a fairly expensive operation. Even though more code is required to hide and possibly move the text view on resizing, I think it's worth the performance gain. – barndog Nov 19 '13 at 08:46