403

How can I add a placeholder in a UITextView, similar to the one you can set for UITextField, in Swift?

אורי orihpt
  • 2,358
  • 2
  • 16
  • 41
StevenZ
  • 6,983
  • 5
  • 16
  • 18
  • This is an age old problem in iOS development with UITextView. I've written subclasses like the one mentioned here: http://stackoverflow.com/a/1704469/1403046 . The benefit is that you can still have a delegate, as well as use the class in multiple places without having to re-implement the logic. – cjwirth Dec 26 '14 at 03:28
  • How would I use your subclass, while using swift for the project. Using a bridge file? – StevenZ Dec 26 '14 at 03:49
  • You could do that, or re-implement it in Swift. The code in the answer is longer than it really has to be. The main point being to show/hide the label you add in the method you get notified for when the text changes. – cjwirth Dec 26 '14 at 03:52
  • You can use UIFloatLabelTextView sample from GitHub. This position placeholder on top while writing. Really interesting one! https://github.com/ArtSabintsev/UIFloatLabelTextView – Jayprakash Dubey Aug 01 '16 at 08:14
  • 3
    Honestly, the easiest way to accomplish this is to have a custom textView and just add placeholder text that is drawn onto the textView when no text is present.... Ever other answer so far has been a far overcomplicated version of this that involves problematic state management (including false positives for when text should/shouldn't does/doesn't exist) – TheCodingArt Dec 31 '16 at 19:09
  • @TheCodingArt - this was created to address that concern: http://stackoverflow.com/a/28271069/2079103 – clearlight Jan 31 '17 at 01:16
  • @clearlight that's still more complicated than my solution lol. It's also toggling different view states and different types of classes (a UILabel and a UITextView). – TheCodingArt Feb 01 '17 at 01:12
  • @TheCodingArt. Would love to see an example. – clearlight Feb 01 '17 at 01:14
  • @clearlight it's one of the answers below lol: http://stackoverflow.com/questions/27652227/text-view-placeholder-swift/31952339#31952339 – TheCodingArt Feb 01 '17 at 01:15
  • `UITextView` has a placeholder property, you just have to set it within IB or via Keypath. No need for subclassing unless additional logic needs to be applied to the state. https://stackoverflow.com/questions/27652227/text-view-uitextview-placeholder-swift/55661099#55661099 – Alex Chase Aug 06 '19 at 04:04

44 Answers44

804

Updated for Swift 4

UITextView doesn't inherently have a placeholder property so you'd have to create and manipulate one programmatically using UITextViewDelegate methods. I recommend using either solution #1 or #2 below depending on the desired behavior.

Note: For either solution, add UITextViewDelegate to the class and set textView.delegate = self to use the text view’s delegate methods.


Solution #1 - If you want the placeholder to disappear as soon as the user selects the text view:

First set the UITextView to contain the placeholder text and set it to a light gray color to mimic the look of a UITextField's placeholder text. Either do so in the viewDidLoad or upon the text view's creation.

textView.text = "Placeholder"
textView.textColor = UIColor.lightGray

Then when the user begins to edit the text view, if the text view contains a placeholder (i.e. if its text color is light gray) clear the placeholder text and set the text color to black in order to accommodate the user's entry.

func textViewDidBeginEditing(_ textView: UITextView) {
    if textView.textColor == UIColor.lightGray {
        textView.text = nil
        textView.textColor = UIColor.black
    }
}

Then when the user finishes editing the text view and it's resigned as the first responder, if the text view is empty, reset its placeholder by re-adding the placeholder text and setting its color to light gray.

func textViewDidEndEditing(_ textView: UITextView) {
    if textView.text.isEmpty {
        textView.text = "Placeholder"
        textView.textColor = UIColor.lightGray
    }
}

Solution #2 - If you want the placeholder to show whenever the text view is empty, even if the text view’s selected:

First set the placeholder in the viewDidLoad:

textView.text = "Placeholder"
textView.textColor = UIColor.lightGray

textView.becomeFirstResponder()

textView.selectedTextRange = textView.textRange(from: textView.beginningOfDocument, to: textView.beginningOfDocument)

(Note: Since the OP wanted to have the text view selected as soon as the view loads, I incorporated text view selection into the above code. If this is not your desired behavior and you do not want the text view selected upon view load, remove the last two lines from the above code chunk.)

Then utilize the shouldChangeTextInRange UITextViewDelegate method, like so:

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

    // Combine the textView text and the replacement text to
    // create the updated text string
    let currentText:String = textView.text
    let updatedText = (currentText as NSString).replacingCharacters(in: range, with: text)

    // If updated text view will be empty, add the placeholder
    // and set the cursor to the beginning of the text view
    if updatedText.isEmpty {

        textView.text = "Placeholder"
        textView.textColor = UIColor.lightGray

        textView.selectedTextRange = textView.textRange(from: textView.beginningOfDocument, to: textView.beginningOfDocument)
    }

    // Else if the text view's placeholder is showing and the
    // length of the replacement string is greater than 0, set 
    // the text color to black then set its text to the
    // replacement string
     else if textView.textColor == UIColor.lightGray && !text.isEmpty {
        textView.textColor = UIColor.black
        textView.text = text
    }

    // For every other case, the text should change with the usual
    // behavior...
    else {
        return true
    }

    // ...otherwise return false since the updates have already
    // been made
    return false
}

And also implement textViewDidChangeSelection to prevent the user from changing the position of the cursor while the placeholder's visible. (Note: textViewDidChangeSelection is called before the view loads so only check the text view's color if the window is visible):

func textViewDidChangeSelection(_ textView: UITextView) {
    if self.view.window != nil {
        if textView.textColor == UIColor.lightGray {
            textView.selectedTextRange = textView.textRange(from: textView.beginningOfDocument, to: textView.beginningOfDocument)
        }
    }
}
Lyndsey Scott
  • 37,080
  • 10
  • 92
  • 128
  • @StevenR You have to delete textViewDidBeginEditing and textViewDidEndEditing – Lyndsey Scott Dec 26 '14 at 05:33
  • @LyndseyScott This really helped me too. But I don't understand why you needed to combine the textview text and replacement text into the "updatedText" variable. So messing around, I tried - `if countElements(text) == 0` - which seemed to also work. Looking at the "shouldChangeTextInRange" method, it sounds like the "text" parameter is for the replacement new text, so therefore it seems like you could test if the text value is 0 or not, instead of the updatedText variable. Is there an issue I missing with this idea? – ScottEdwards2000 Dec 27 '14 at 02:28
  • @ScottEdwards2000 That won't work. For example, if you type a string with 10 characters then hit backspace once, the replacement text will have length == 0 and the placeholder will show up even though there was still text in the text field. The only way to tell if the updated text will have a length equal to 0 is to do what I've done with updatedText and combine the text view with its replacement. – Lyndsey Scott Dec 27 '14 at 02:32
  • However, I do notice using "text" that when I hit the backspace it deletes all letters, versus with updatedText it deletes one letter at a time, which is better. Do you know why this is? – ScottEdwards2000 Dec 27 '14 at 02:35
  • @ScottEdwards2000 Yes, because just like I said, "if you type a string with 10 characters then hit backspace once, the replacement text will have length == 0 and the placeholder will show up even though there was still text in the text field." So the old text will delete and the placeholder will show if you simply do `if countElements(text) == 0`. That's why you *have* to do it using some sort of `updatedText` variable combining the old text with the replacement text in order to be able to detect backspaces appropriately. – Lyndsey Scott Dec 27 '14 at 02:37
  • when you say the "replacement text", do you mean the 3rd argument of the function? I don't see where u call the function with that parameter in your code. This is quite confusing to a newbie! Thanks!! – ScottEdwards2000 Dec 27 '14 at 03:30
  • @ScottEdwards2000 func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, *replacementText* text: String) -> Bool ... replacementText is "text". – Lyndsey Scott Dec 27 '14 at 03:31
  • @LyndseyScott I finally got it after reading your comments and stepping through with the debugger. Your solution is exactly what I was looking for and thank you so much! – ScottEdwards2000 Dec 27 '14 at 19:39
  • @LyndseyScott, well, I tried to make an edit to your post, but it was denied, so maybe a comment will be better. `countElements` is used twice in your code, this has been changed in a newer version of swift to just `count`. – TaylorAllred May 01 '15 at 00:37
  • hi, this works perfectly but how can I make the first letter capital ? Though I set the Capitalisation for Sentences in the Xcode interface builder it does not work. – Ankahathara Jun 11 '15 at 06:38
  • @Ankahathara Are you asking how to make the first letter of the UITextView text capital? Sounds like a completely different question that you should perhaps post/look for elsewhere... – Lyndsey Scott Jun 11 '15 at 09:51
  • Swift 1.2 iOs 8.4 , comparing to `nil` is not working for me, instead do a comparison to `""` , absence of text. – Juan Boero Aug 11 '15 at 20:56
  • @JuanPabloBoero Other than this line: `if self.view.window != nil` I don't see any nil comparisons in the code I wrote... Instead of comparing text with "", I used the `isEmpty` property: `if textView.text.isEmpty {`. I'd recommend using `isEmpty` over doing a comparison to "". – Lyndsey Scott Aug 11 '15 at 21:22
  • This is a pretty bad answer with far to many states you must take into consideration ...... – TheCodingArt Aug 11 '15 at 21:32
  • @TheCodingArt "with far to [sic] many states you must take into consideration" Such as...? – Lyndsey Scott Aug 11 '15 at 21:33
  • @LyndseyScott this alone is a state problem: textView.text = "Placeholder".. When you grab the placeholder text from any view that naturally supports placeholder text, you don't expect to receive the placeholder text from the text property. This means to get that functionality, you have to juggle the difference between text, attributedText, placeholderText, etc. – TheCodingArt Aug 11 '15 at 21:35
  • Using the text property alone and juggling those states causes many many unexpected headaches down the line. – TheCodingArt Aug 11 '15 at 21:36
  • @TheCodingArt "When you grab the placeholder text from any view that naturally supports placeholder text..." I think you're misunderstanding the point of this question/answer. `UITextView`'s don't have a placeholder property, thus the need for this workaround. – Lyndsey Scott Aug 11 '15 at 21:36
  • @LyndseyScott I do understand the point... and I have provided an answer. I'm pointing out major flaws with this answer and why it's not a good way to go about it. – TheCodingArt Aug 11 '15 at 21:37
  • I tried adding placeholder text to multiple textviews and giving each their own tag but that didn't work. They all reset to the first one's initial text. Any ideas? –  Aug 14 '15 at 18:43
  • @theActuary Sounds like a code issue so I'd recommend posting your question on Stack Overflow. – Lyndsey Scott Aug 14 '15 at 18:44
  • I don't know if you mention this before but: Is necessary to put UITextViewDelegate and do delegate from your textView to your view. – Dasoga Sep 15 '15 at 22:27
  • 7
    Hi, make sure you **set your view controller as the delegate for your textView**. You can achieve this by creating an outlet from your textView to your viewController. Then use `yourTextField.delegate = self`. If you do not do this, the `textViewDidBeginEditing` and the `textViewDidEndEditing` functions will not work. – Ujjwal-Nadhani Apr 23 '16 at 17:05
  • That's mess, you use color as your model. It's in opposite to all design rules. Please hold metadata in other place. Tag or something. – Leszek Zarna Jun 26 '17 at 08:20
  • 2
    The code is not compiling, I am having an error as `Cannot convert value of type 'NSRange' (aka '_NSRange') to expected argument type 'Range' (aka 'Range')`. – iPeter Jun 27 '17 at 15:02
  • @iPeter In Swift 3? – Lyndsey Scott Jun 27 '17 at 15:27
  • Yes Ma'am! @LyndseyScott – iPeter Jun 28 '17 at 06:39
  • @iPeter try this: `let updatedText = currentText?.replacingCharacters(in: Range(range, in: currentText!)!, with: text)` – user3647894 Jul 14 '17 at 16:28
  • I am having the same issue as @iPeter in Swift 3. Won't compile and gives that error on the `let updatedText =` line. – Baylor Mitchell Jul 14 '17 at 22:04
  • 4
    I have found the solution and I will attach the revised code @iPeter . The current text must be in NSString formant: `let currentText = textView.text as NSString?`. Transform the `let updatedText =` line to `let updatedText = currentText?.replacingCharacters(in: range, with: text)`. Finally, transform the `if updatedText.isEmpty` line to `if (updatedText?.isEmpty)! {`. That should do the trick! – Baylor Mitchell Jul 14 '17 at 22:21
  • Help... When I paste some text to textview, it change placeholder to my pasted text, but text color still gray although textView.textColor = UIColor.black called – Tà Truhoada May 07 '18 at 10:47
  • 1
    @TàTruhoada I’ve updated the solution to handle copy and paste scenarios – Lyndsey Scott May 07 '18 at 15:34
  • @LyndseyScott else if textView.textColor == UIColor.lightGray && !text.isEmpty { textView.textColor = UIColor.black textView.text = text } if I return false, the first charactor will not show – Tà Truhoada May 08 '18 at 03:45
  • @TàTruhoada Are you sure you’ve entered all the code as written? Including the else { return true } block? – Lyndsey Scott May 08 '18 at 05:43
  • @TàTruhoada It personally works for me. If you’d like to post a link to your code, I can take a look. – Lyndsey Scott May 08 '18 at 14:49
  • @LyndseyScott Thank you! but I can't share my project. I will try to solve my problem. Thanks for your support! – Tà Truhoada May 09 '18 at 03:18
  • This does not behave like a UITextField, whereby the placeholder dissapears when text is entered (not when editing begins) – agandi May 18 '18 at 14:40
  • @agandi The second part of the answer has that behavior. – Lyndsey Scott May 18 '18 at 16:51
  • 1
    @LyndseyScott After a long time, today I come back to fix my problem. And then I realize my code is still holding an old line in your code. Instead of using "textView.text = text", I used "textView.text = nil", so it's not working. After edited, it works like a charm! Thank you so much! – Tà Truhoada May 29 '18 at 10:09
  • @LyndseyScott a silly question, since I am not native English speaker, what is the reason of not using period in comment? I googled but can not find a useful information. Thank you. – Houcheng Sep 17 '18 at 01:46
  • @LyndseyScott I'm also an iOS developer at BT (the best company in the world) let's go out. PM me – User123335511231 Jan 14 '19 at 15:03
  • 3
    @LyndseyScott setting `textView.selectedTextRange` from within `func textViewDidChangeSelection(_ textView: UITextView)` causes an infinite loop... – MikeG Feb 05 '19 at 18:37
  • `UITextView` _does_ have a `placeholder` property. It can be set in IB or via Keypath – Alex Chase Aug 06 '19 at 04:06
  • How to use your answer to allow specific length of text in the `UITextView`? @LyndseyScott – Hemang Aug 08 '19 at 07:53
  • For the first time, cursor is not moving to beginning of placeholder, so i put the cursor mover code i.e. in "textViewDidChangeSelection" delegate to also add in "textViewShouldBeginEditing" and now it's working perfectly. – g212gs Jun 09 '20 at 07:09
  • At least atm, `UITextView.text` is implicitly unwrapped (`!`), so for the first solution, the property should not be set to `nil`, rather `""` – Cloud Aug 11 '20 at 17:13
  • 30
    It's absolutely shocking that (iOS) developers should need to go to these lengths for something so simple. This is basic functionality that should have been part of the TextView control a long time ago and I can't understand why Apple hasn't addressed this yet. – Kenny Aug 31 '20 at 13:01
  • This solution is very simple and seems bug free while it is hardly reusable you need to write those code every time you use this feature – Bigair Oct 05 '20 at 12:17
  • I get a warning with "Snapshotting a view ... requires afterScreenUpdates:YES". It's caused by setting a text in `textViewDidBeginEditing`. To get rid of this warning, move the text and color setting to `textViewShouldBeginEditing` and return true – boweidmann Jan 31 '21 at 11:55
  • I can't cut text of textView in second solution :( It is just deleted but not copied. – Flatout Mar 29 '21 at 15:16
  • 1
    On iOS 15, when I select a text from keyboard suggestion, it gets written in the textView 2 times. How to solve this? @LyndseyScott – Raj D Oct 21 '21 at 14:23
317

Floating Placeholder


It's simple, safe and reliable to position a placeholder label above a text view, set its font, color and manage placeholder visibility by tracking changes to the text view's character count.

Update: Incorporated suggestions made by @RakshithaMurangaRodrigo in Feb 10, '23 comment

Swift 5:

class NotesViewController : UIViewController {

    @IBOutlet var textView : UITextView!
    var placeholderLabel : UILabel!
        
    override func viewDidLoad() {
        super.viewDidLoad()
    
        textView.delegate = self
        placeholderLabel = UILabel()
        placeholderLabel.text = "Enter some text..."
        placeholderLabel.font = .italicSystemFont(ofSize: (textView.font?.pointSize)!)
        placeholderLabel.sizeToFit()
        textView.addSubview(placeholderLabel)
        placeholderLabel.frame.origin = CGPoint(x: 5, y: (textView.font?.pointSize)! / 2)
        placeholderLabel.textColor = .tertiaryLabel
        placeholderLabel.isHidden = !textView.text.isEmpty
    }
}

extension NotesViewController : UITextViewDelegate {
    func textViewDidChange(_ textView: UITextView) {
        placeholderLabel?.isHidden = !textView.text.isEmpty
    }
    func textViewDidEndEditing(_ textView: UITextView) {         
        placeholderLabel?.isHidden = !textView.text.isEmpty     
    }  
    func textViewDidBeginEditing(_ textView: UITextView) {         
        placeholderLabel?.isHidden = true     
    }
}

clearlight
  • 12,255
  • 11
  • 57
  • 75
  • 32
    This method is undeniably the simplest and least error-prone I've found. Fabulous. – David Feb 03 '15 at 02:17
  • 6
    It is a good method but label text may go out of bound if placeholder text is very long.. – Aks Feb 03 '15 at 07:02
  • 5
    Simple to use and integrates nicely with existing behavior. The placeholder message should not be so verbose anyway however wouldn't setting the lines to 0 take care of that issue? – Tommie C. Nov 04 '15 at 13:59
  • 3
    I generally prefer this method, though it is problematic if the text is center aligned because the cursor will be centered on top of the placeholder instead of to the left of it. – blwinters Jun 21 '17 at 15:31
  • @clearlight In my case, I have a text view for entering the name of an object at the top of a grouped tableview, centered between two buttons. (Similar to the Trello app, but centered.) It starts with one line and increases its height as new lines are needed, using `UITableViewAutomaticDimension`, `beginUpdates()`, `endUpdates()`. When the text is empty and the placeholder is visible (single line), I want it to look and behave more like a center aligned `UITextField`, which I'm pretty sure places the cursor to the left of a centered placeholder. – blwinters Jun 27 '17 at 00:28
  • best and cleanest answer – nsleche Jul 14 '21 at 01:09
  • This is a much more elegant and reliable solution! Definitely suggest this one. – msweet168 Nov 11 '21 at 08:43
  • 1
    Thank you, this is Nice and easy approach, very clean, and I could modify some what father based on this answer.... `func textViewDidChange(_ textView: UITextView) { self.placeholderLabel?.isHidden = !self.textView.text.isEmpty }` `func textViewDidEndEditing(_ textView: UITextView) { self.placeholderLabel?.isHidden = !self.textView.text.isEmpty }` `func textViewDidBeginEditing(_ textView: UITextView) { self.placeholderLabel?.isHidden = true }` – Rakshitha Muranga Rodrigo Feb 10 '23 at 03:37
  • when select all text,it crash ```[XXXController selectAll:]: unrecognized selector sent to instance 0x14e821800' terminating with uncaught exception of type NSException``` – iHTCboy Feb 19 '23 at 10:39
  • @IHTCboy I don't see how that crash could be in any way related to this answer, as this is very straightforward basic UIKit stuff unlikely to product side-effects if your class is coded properly, and you don't have any dangling or undefined references. I think something else is going on in your app that's making it fragile. – clearlight Feb 22 '23 at 04:58
37

Swift:

Add your text view programmatically or via Interface Builder, if the last, create the outlet:

@IBOutlet weak var yourTextView: UITextView!

Please add the delegate (UITextViewDelegate):

class ViewController: UIViewController, UITextViewDelegate {

In the viewDidLoad method, do add the following:

override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

    yourTextView.delegate = self
    yourTextView.text = "Placeholder text goes right here..."
    yourTextView.textColor = UIColor.lightGray

Now let me introduce the magic part, add this function:

func textViewDidBeginEditing(_ textView: UITextView) {

    if yourTextView.textColor == UIColor.lightGray {
        yourTextView.text = ""
        yourTextView.textColor = UIColor.black
    }
}

Do note that this will execute whenever editing starts, there we will check conditions to tell the state, using the color property. Setting text to nil i do not recommend. Right after that, we set the text color to desired, in this case, black.

Now add this function too:

func textViewDidEndEditing(_ textView: UITextView) {

    if yourTextView.text == "" {

        yourTextView.text = "Placeholder text ..."
        yourTextView.textColor = UIColor.lightGray
    }
}

Let me insist, do not compare to nil, i have already tried that and it would not work. We then set the values back to placeholder style, and set the color back to placeholder color because it is a condition to check in textViewDidBeginEditing.

Juan Boero
  • 6,281
  • 1
  • 44
  • 62
31

I am surprised that no one mentioned NSTextStorageDelegate. UITextViewDelegate's methods will only be triggered by user interaction, but not programmatically. E.g. when you set a text view's text property programmatically, you'll have to set the placeholder's visibility yourself, because the delegate methods will not be called.

However, with NSTextStorageDelegate's textStorage(_:didProcessEditing:range:changeInLength:) method, you'll be notified of any change to the text, even if it's done programmatically. Just assign it like this:

textView.textStorage.delegate = self

(In UITextView, this delegate property is nil by default, so it won't affect any default behaviour.)

Combine it with the UILabel technique @clearlight demonstrates, one can easily wrap the whole UITextView's placeholder implementation into an extension.

extension UITextView {

    private class PlaceholderLabel: UILabel { }

    private var placeholderLabel: PlaceholderLabel {
        if let label = subviews.compactMap( { $0 as? PlaceholderLabel }).first {
            return label
        } else {
            let label = PlaceholderLabel(frame: .zero)
            label.font = font
            addSubview(label)
            return label
        }
    }

    @IBInspectable
    var placeholder: String {
        get {
            return subviews.compactMap( { $0 as? PlaceholderLabel }).first?.text ?? ""
        }
        set {
            let placeholderLabel = self.placeholderLabel
            placeholderLabel.text = newValue
            placeholderLabel.numberOfLines = 0
            let width = frame.width - textContainer.lineFragmentPadding * 2
            let size = placeholderLabel.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
            placeholderLabel.frame.size.height = size.height
            placeholderLabel.frame.size.width = width
            placeholderLabel.frame.origin = CGPoint(x: textContainer.lineFragmentPadding, y: textContainerInset.top)

            textStorage.delegate = self
        }
    }

}

extension UITextView: NSTextStorageDelegate {

    public func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorageEditActions, range editedRange: NSRange, changeInLength delta: Int) {
        if editedMask.contains(.editedCharacters) {
            placeholderLabel.isHidden = !text.isEmpty
        }
    }

}

Note that the use of a private (nested) class called PlaceholderLabel. It has no implementation at all, but it provides us a way to identify the placeholder label, which is far more 'swifty' than using the tag property.

With this approach, you can still assign the delegate of the UITextView to someone else.

You don't even have to change your text views' classes. Just add the extension(s) and you will be able to assign a placeholder string to every UITextView in your project, even in the Interface Builder.

I left out the implementation of a placeholderColor property for clarity reasons, but it can be implemented for just a few more lines with a similar computed variable to placeholder.

yesleon
  • 928
  • 11
  • 14
20

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 placeholder text fully separated from any outside classes looking at the field's text property.

Any improvement suggestions are welcome, especially if there's any way to pull iOS's placeholder color programatically, rather than hard-coding it.

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 { super.text = newValue }
    }
    @IBInspectable var placeholderText: String = ""
    @IBInspectable var placeholderTextColor: UIColor = UIColor(red: 0.78, green: 0.78, blue: 0.80, alpha: 1.0) // Standard iOS placeholder color (#C7C7CD). See https://stackoverflow.com/questions/31057746/whats-the-default-color-for-placeholder-text-in-uitextfield
    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 func becomeFirstResponder() -> Bool {
        // If the current text is the placeholder, remove it
        if showingPlaceholder {
            text = nil
            textColor = nil // Put the text back to the default, unmodified color
            showingPlaceholder = false
        }
        return super.becomeFirstResponder()
    }
    
    override func resignFirstResponder() -> Bool {
        // If there's no text, put the placeholder back
        if text.isEmpty {
            showPlaceholderText()
        }
        return super.resignFirstResponder()
    }
    
    private func showPlaceholderText() {
        showingPlaceholder = true
        textColor = placeholderTextColor
        text = placeholderText
    }
}
TheNeil
  • 3,321
  • 2
  • 27
  • 52
  • I tried to use in tableview cell. When I set textView text then it does not gets set as showingPlaceholder is true by default. – Nitesh Mar 31 '21 at 07:42
15

I did this by using two different text views:

  1. One in the background that is used as a placeholder.
  2. One in the foreground (with a transparent background) that the user actually types in.

The idea is that once the user starts typing stuff in the foreground view, the placeholder in the background disappears (and reappears if the user deletes everything). So it behaves exactly like a placeholder for the single line text field.

Here's the code I used for it. Note that descriptionField is the field the user types in and descriptionPlaceholder is the one in the background.

func textViewDidChange(descriptionField: UITextView) {
    if descriptionField.text.isEmpty == false {
        descriptionPlaceholder.text = ""
    } else {
        descriptionPlaceholder.text = descriptionPlaceholderText
    }
}
yesthisisjoe
  • 1,987
  • 2
  • 16
  • 32
  • 1
    This way is a bit hacky but it's far and away the easiest and generates exactly the results you want. Good idea – William T. Jun 11 '17 at 18:14
11

I tried to make code convenient from clearlight's answer.

extension UITextView{

    func setPlaceholder() {

        let placeholderLabel = UILabel()
        placeholderLabel.text = "Enter some text..."
        placeholderLabel.font = UIFont.italicSystemFont(ofSize: (self.font?.pointSize)!)
        placeholderLabel.sizeToFit()
        placeholderLabel.tag = 222
        placeholderLabel.frame.origin = CGPoint(x: 5, y: (self.font?.pointSize)! / 2)
        placeholderLabel.textColor = UIColor.lightGray
        placeholderLabel.isHidden = !self.text.isEmpty

        self.addSubview(placeholderLabel)
    }

    func checkPlaceholder() {
        let placeholderLabel = self.viewWithTag(222) as! UILabel
        placeholderLabel.isHidden = !self.text.isEmpty
    }

}

usage

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

func textViewDidChange(_ textView: UITextView) {
    textView.checkPlaceholder()
}
Community
  • 1
  • 1
Leonardo
  • 155
  • 1
  • 7
  • 1
    A couple of issues. (1) As an extension that assumes and 'borrows' a UIView tag property value, there is risk someone might use the same tag in their own view hierarchy, unaware of the extension's usage, creating an extremely difficult to diagnose bug. Things like that don't belong in library code or extensions. (2) It still requires the caller to declare a delegate. The [Floating Placeholder](http://stackoverflow.com/a/28271069/2079103) avoids hacks, has a tiny footprint, is simple and entirely local, which makes it a safe bet. – clearlight Feb 28 '17 at 08:24
10

Here is what I'm using for getting this job done.

@IBDesignable class UIPlaceholderTextView: UITextView {
    
    var placeholderLabel: UILabel?
    
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        sharedInit()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        sharedInit()
    }
    
    override func prepareForInterfaceBuilder() {
        sharedInit()
    }
    
    func sharedInit() {
        refreshPlaceholder()
        NotificationCenter.default.addObserver(self, selector: #selector(textChanged), name: UITextView.textDidChangeNotification, object: nil)
    }

    @IBInspectable var placeholder: String? {
        didSet {
            refreshPlaceholder()
        }
    }

    @IBInspectable var placeholderColor: UIColor? = .darkGray {
        didSet {
            refreshPlaceholder()
        }
    }
    
    @IBInspectable var placeholderFontSize: CGFloat = 14 {
        didSet {
            refreshPlaceholder()
        }
    }
    
    func refreshPlaceholder() {
        if placeholderLabel == nil {
            placeholderLabel = UILabel()
            let contentView = self.subviews.first ?? self
            
            contentView.addSubview(placeholderLabel!)
            placeholderLabel?.translatesAutoresizingMaskIntoConstraints = false
            
            placeholderLabel?.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: textContainerInset.left + 4).isActive = true
            placeholderLabel?.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: textContainerInset.right + 4).isActive = true
            placeholderLabel?.topAnchor.constraint(equalTo: contentView.topAnchor, constant: textContainerInset.top).isActive = true
            placeholderLabel?.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: textContainerInset.bottom).isActive = true
        }
        placeholderLabel?.text = placeholder
        placeholderLabel?.textColor = placeholderColor
        placeholderLabel?.font = UIFont.systemFont(ofSize: placeholderFontSize)
    }
    
    @objc func textChanged() {
        if self.placeholder?.isEmpty ?? true {
            return
        }
        
        UIView.animate(withDuration: 0.25) {
            if self.text.isEmpty {
                self.placeholderLabel?.alpha = 1.0
            } else {
                self.placeholderLabel?.alpha = 0.0
            }
        }
    }
    
    override var text: String! {
        didSet {
            textChanged()
        }
    }

}

I know there're several approaches similar to this but the benefits from this one are that it can:

  • Set placeholder text, font size and color in IB.
  • No longer shows the warning of "Scroll View has ambiguous scrollable content" in IB.
  • Add animation to show/hide of placeholder.
Axel Guilmin
  • 11,454
  • 9
  • 54
  • 64
Pei
  • 11,452
  • 5
  • 41
  • 45
  • Where do you remove an observer? – fnc12 Sep 08 '21 at 08:26
  • What observer do you mean? – Pei Sep 09 '21 at 05:52
  • every line `NotificationCenter.default.addObserver` must have a counter line with `NotificationCenter.default.removeObserver` – fnc12 Sep 09 '21 at 10:19
  • From iOS 9 and on, you don't need to explicitly remove observers: https://developer.apple.com/documentation/foundation/notificationcenter/1413994-removeobserver – Pei Sep 09 '21 at 10:56
  • Great solution, but I had to use `let contentView = self` or I couldn't see the placeholder as the first subview had a zero sized rect. – Darren Jan 17 '23 at 16:24
9

Swift:

Add your TextView @IBOutlet:

@IBOutlet weak var txtViewMessage: UITextView!

In the viewWillAppear method, do add the following :

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    txtViewMessage.delegate = self    // Give TextViewMessage delegate Method
    
    txtViewMessage.text = "Place Holder Name"
    txtViewMessage.textColor = UIColor.lightGray
}

Please add the Delegate Using extension (UITextViewDelegate):

// MARK: - UITextViewDelegate
extension ViewController: UITextViewDelegate {

    func textViewDidBeginEditing(_ textView: UITextView) {

        if !txtViewMessage.text!.isEmpty && txtViewMessage.text! == "Place Holder Name" {
            txtViewMessage.text = ""
            txtViewMessage.textColor = UIColor.black
        }
    }

    func textViewDidEndEditing(_ textView: UITextView) {
    
        if txtViewMessage.text.isEmpty {
            txtViewMessage.text = "Place Holder Name"
            txtViewMessage.textColor = UIColor.lightGray
        }
    }
}
6

SET value in view load

    txtVw!.autocorrectionType = UITextAutocorrectionType.No
    txtVw!.text = "Write your Placeholder"
    txtVw!.textColor = UIColor.lightGrayColor()



func textViewDidBeginEditing(textView: UITextView) {
    if (txtVw?.text == "Write your Placeholder")

    {
        txtVw!.text = nil
        txtVw!.textColor = UIColor.blackColor()
    }
}

func textViewDidEndEditing(textView: UITextView) {
    if txtVw!.text.isEmpty
    {
        txtVw!.text = "Write your Placeholder"
        txtVw!.textColor = UIColor.lightGrayColor()
    }
    textView.resignFirstResponder()
}
6

One more solution (Swift 3):

import UIKit

protocol PlaceholderTextViewDelegate {
    func placeholderTextViewDidChangeText(_ text:String)
    func placeholderTextViewDidEndEditing(_ text:String)
}

final class PlaceholderTextView: UITextView {

    var notifier:PlaceholderTextViewDelegate?

    var placeholder: String? {
        didSet {
            placeholderLabel?.text = placeholder
        }
    }
    var placeholderColor = UIColor.lightGray
    var placeholderFont = UIFont.appMainFontForSize(14.0) {
        didSet {
            placeholderLabel?.font = placeholderFont
        }
    }

    fileprivate var placeholderLabel: UILabel?

    // MARK: - LifeCycle

    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
        NotificationCenter.default.addObserver(self, selector: #selector(PlaceholderTextView.textDidChangeHandler(notification:)), name: .UITextViewTextDidChange, object: nil)

        placeholderLabel = UILabel()
        placeholderLabel?.textColor = placeholderColor
        placeholderLabel?.text = placeholder
        placeholderLabel?.textAlignment = .left
        placeholderLabel?.numberOfLines = 0
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        placeholderLabel?.font = placeholderFont

        var height:CGFloat = placeholderFont.lineHeight
        if let data = placeholderLabel?.text {

            let expectedDefaultWidth:CGFloat = bounds.size.width
            let fontSize:CGFloat = placeholderFont.pointSize

            let textView = UITextView()
            textView.text = data
            textView.font = UIFont.appMainFontForSize(fontSize)
            let sizeForTextView = textView.sizeThatFits(CGSize(width: expectedDefaultWidth,
                                                               height: CGFloat.greatestFiniteMagnitude))
            let expectedTextViewHeight = sizeForTextView.height

            if expectedTextViewHeight > height {
                height = expectedTextViewHeight
            }
        }

        placeholderLabel?.frame = CGRect(x: 5, y: 0, width: bounds.size.width - 16, height: height)

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

    func textDidChangeHandler(notification: Notification) {
        layoutSubviews()
    }

}

extension PlaceholderTextView : UITextViewDelegate {
    // MARK: - UITextViewDelegate
    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        if(text == "\n") {
            textView.resignFirstResponder()
            return false
        }
        return true
    }

    func textViewDidChange(_ textView: UITextView) {
        notifier?.placeholderTextViewDidChangeText(textView.text)
    }

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

result

enter image description here

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

We can implement textview PlaceHolder quite easily if we are Using pod IQKeyboardManagerSwift in our project just have to follow 4 steps

  1. We have to assign the class IQTextView to our TextView class.
  2. We have to import the IQKeyboardManagerSwift in our Controller page
  3. Last but not least make the outlet of the textView on the Controller page // if you want :)
  4. Give the textView some place holder text through the storyboard inspectable
JustSomeGuy
  • 3,677
  • 1
  • 23
  • 31
5

A simple and quick solution that works for me is:

@IBDesignable
class PlaceHolderTextView: UITextView {

    @IBInspectable var placeholder: String = "" {
         didSet{
             updatePlaceHolder()
        }
    }

    @IBInspectable var placeholderColor: UIColor = UIColor.gray {
        didSet {
            updatePlaceHolder()
        }
    }

    private var originalTextColor = UIColor.darkText
    private var originalText: String = ""

    private func updatePlaceHolder() {

        if self.text == "" || self.text == placeholder  {

            self.text = placeholder
            self.textColor = placeholderColor
            if let color = self.textColor {

                self.originalTextColor = color
            }
            self.originalText = ""
        } else {
            self.textColor = self.originalTextColor
            self.originalText = self.text
        }

    }

    override func becomeFirstResponder() -> Bool {
        let result = super.becomeFirstResponder()
        self.text = self.originalText
        self.textColor = self.originalTextColor
        return result
    }
    override func resignFirstResponder() -> Bool {
        let result = super.resignFirstResponder()
        updatePlaceHolder()

        return result
    }
}
Sepand Y.
  • 81
  • 2
  • 8
4

Swift 3.2

extension EditProfileVC:UITextViewDelegate{

    func textViewDidBeginEditing(_ textView: UITextView) {
        if textView.textColor == UIColor.lightGray {
            textView.text = nil
            textView.textColor = UIColor.black
       }
    }
    func textViewDidEndEditing(_ textView: UITextView) {
        if textView.text.isEmpty {
            textView.text = "Placeholder"
            textView.textColor = UIColor.lightGray
        }
    }
}

First when user start editing textViewDidBeginEditing call and then check the if colour of text grey means user didn't write anything then set as textview nil and change the colour to black for user texting.

When user end editing textViewDidEndEditing is call and check if user doesn't write anything in textview then text set as grey colour with text "PlaceHolder"

V D Purohit
  • 1,179
  • 1
  • 10
  • 23
3

Here is my way of solving this problem (Swift 4):

The idea was to make the simplest possible solution which allows to use placeholders of different colors, resizes to placeholders size, will not overwrite a delegate meanwhile keeping all UITextView functions work as expected.

import UIKit

class PlaceholderTextView: UITextView {
    var placeholderColor: UIColor = .lightGray
    var defaultTextColor: UIColor = .black

    private var isShowingPlaceholder = false {
        didSet {
            if isShowingPlaceholder {
                text = placeholder
                textColor = placeholderColor
            } else {
                textColor = defaultTextColor
            }
        }
    }

    var placeholder: String? {
        didSet {
            isShowingPlaceholder = !hasText
        }
    }

    @objc private func textViewDidBeginEditing(notification: Notification) {
        textColor = defaultTextColor
        if isShowingPlaceholder { text = nil }
    }

    @objc private func textViewDidEndEditing(notification: Notification) {
        isShowingPlaceholder = !hasText
    }

    // MARK: - Construction -
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        setup()
    }

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

    private func setup() {
        NotificationCenter.default.addObserver(self, selector: #selector(textViewDidBeginEditing(notification:)), name: UITextView.textDidBeginEditingNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(textViewDidEndEditing(notification:)), name: UITextView.textDidEndEditingNotification, object: nil)
    }

    // MARK: - Destruction -
    deinit { NotificationCenter.default.removeObserver(self) }
}
fewlinesofcode
  • 3,007
  • 1
  • 13
  • 30
3

Swift Answer

Here is the custom class, that animates placeholder.

class CustomTextView: UITextView {
    
    //   MARK: - public
    
    public var placeHolderText: String? = "Enter Reason.."
    
    public lazy var placeHolderLabel: UILabel! = {
        let placeHolderLabel = UILabel(frame: .zero)
        placeHolderLabel.numberOfLines = 0
        placeHolderLabel.backgroundColor = .clear
        placeHolderLabel.alpha = 0.5
        return placeHolderLabel
    }()
    
    //   MARK: - Init
    
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        enableNotifications()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        enableNotifications()
    }
    
    func setup() {
        placeHolderLabel.frame = CGRect(x: 8, y: 8, width: self.bounds.size.width - 16, height: 15)
        placeHolderLabel.sizeToFit()
    }
    
    //   MARK: - Cycle
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 8, right: 8)
        returnKeyType = .done
        addSubview(placeHolderLabel)
        placeHolderLabel.frame = CGRect(x: 8, y: 8, width: self.bounds.size.width - 16, height: 15)
        placeHolderLabel.textColor = textColor
        placeHolderLabel.font = font
        placeHolderLabel.text = placeHolderText
        bringSubviewToFront(placeHolderLabel)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        setup()
    }
    
    //   MARK: - Notifications
    
    private func enableNotifications() {
        NotificationCenter.default.addObserver(self, selector: #selector(textDidChangeNotification(_:)), name: UITextView.textDidChangeNotification , object: nil)
    }
    
    @objc func textDidChangeNotification(_ notify: Notification) {
        guard self == notify.object as? UITextView else { return }
        guard placeHolderText != nil else { return }
        
        UIView.animate(withDuration: 0.25, animations: {
            self.placeHolderLabel.alpha = (self.text.count == 0) ? 0.5 : 0
        }, completion: nil)
    }
    
}
Community
  • 1
  • 1
Lal Krishna
  • 15,485
  • 6
  • 64
  • 84
2

I don't know why people over complicate this issue so much.... It's fairly straight forward and simple. Here's a subclass of UITextView that provides the requested functionality.

- (void)customInit
{
    self.contentMode = UIViewContentModeRedraw;
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textChanged:) name:UITextViewTextDidChangeNotification object:nil];
}

    - (void)textChanged:(NSNotification *)notification
    {
        if (notification.object == self) {
            if(self.textStorage.length != 0 || !self.textStorage.length) {
                [self setNeedsDisplay];
            }
        }
    }


    #pragma mark - Setters

    - (void)setPlaceholderText:(NSString *)placeholderText withFont:(UIFont *)font
    {
        self.placeholderText = placeholderText;
        self.placeholderTextFont = font;

    }



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

        if (self.textStorage.length != 0) {
            return;
        }

        CGRect inset = CGRectInset(rect, 8, 8);//Default rect insets for textView
        NSDictionary *attributes =  @{NSFontAttributeName: self.placeholderTextFont, NSForegroundColorAttributeName: [UIColor grayColor]};
        [self.placeholderText drawInRect:inset withAttributes:attributes];
    }`
TheCodingArt
  • 3,436
  • 4
  • 30
  • 53
2

This is my ready to use solution if you are working with multiple text views

func textViewShouldBeginEditing(textView: UITextView) -> Bool {        
    // Set cursor to the beginning if placeholder is set
    if textView.textColor == UIColor.lightGrayColor() {
        textView.selectedTextRange = textView.textRangeFromPosition(textView.beginningOfDocument, toPosition: textView.beginningOfDocument)
    }

    return true
}

func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
    // Remove placeholder
    if textView.textColor == UIColor.lightGrayColor() && text.characters.count > 0 {
        textView.text = ""
        textView.textColor = UIColor.blackColor()
    }

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

    return true
}

func textViewDidChange(textView: UITextView) {
    // Set placeholder if text is empty
    if textView.text.isEmpty {
        textView.text = NSLocalizedString("Hint", comment: "hint")
        textView.textColor = UIColor.lightGrayColor()
        textView.selectedTextRange = textView.textRangeFromPosition(textView.beginningOfDocument, toPosition: textView.beginningOfDocument)
    }
}

func textViewDidChangeSelection(textView: UITextView) {
    // Set cursor to the beginning if placeholder is set
    let firstPosition = textView.textRangeFromPosition(textView.beginningOfDocument, toPosition: textView.beginningOfDocument)

    // Do not change position recursively
    if textView.textColor == UIColor.lightGrayColor() && textView.selectedTextRange != firstPosition {
        textView.selectedTextRange = firstPosition
    }
}
nickoff
  • 79
  • 1
  • 7
2

Swift - I wrote a class that inherited UITextView and I added a UILabel as a subview to act as a placeholder.

  import UIKit
  @IBDesignable
  class HintedTextView: UITextView {

      @IBInspectable var hintText: String = "hintText" {
          didSet{
              hintLabel.text = hintText
          }
      }

      private lazy var hintLabel: UILabel = {
          let label = UILabel()
          label.font = UIFont.systemFontOfSize(16)
          label.textColor = UIColor.lightGrayColor()
          label.translatesAutoresizingMaskIntoConstraints = false
          return label
      }()


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

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

      override func prepareForInterfaceBuilder() {
         super.prepareForInterfaceBuilder()
         setupView()
      }

      private func setupView() {

        translatesAutoresizingMaskIntoConstraints = false
        delegate = self
        font = UIFont.systemFontOfSize(16)

        addSubview(hintLabel)

        NSLayoutConstraint.activateConstraints([

           hintLabel.leftAnchor.constraintEqualToAnchor(leftAnchor, constant: 4),
           hintLabel.rightAnchor.constraintEqualToAnchor(rightAnchor, constant: 8),
           hintLabel.topAnchor.constraintEqualToAnchor(topAnchor, constant: 4),
           hintLabel.heightAnchor.constraintEqualToConstant(30)
         ])
        }

      override func layoutSubviews() {
        super.layoutSubviews()
        setupView()
     }

}
  • Great example of using constraints to position the placeholder, however the ideal position of the placeholder is right above where the first character of the text is going to go, so calculating that position based on the the text view's font is better for that because it adapts to whatever size of font is set for the text view. – clearlight Dec 10 '16 at 00:02
2

Swift 3.1

This extension worked well for me: https://github.com/devxoul/UITextView-Placeholder

Here is a code snippet:

Install it via pod:

pod 'UITextView+Placeholder', '~> 1.2'

Import it to your class

import UITextView_Placeholder

And add placeholder property to your already created UITextView

textView.placeholder = "Put some detail"

Thats it... Here how it looks (Third box is a UITextView) enter image description here

Vaibhav Saran
  • 12,848
  • 3
  • 65
  • 75
2

I had to dispatch queue to get my placeholder text to reappear once editing was completed.

func textViewDidBeginEditing(_ textView: UITextView) {

    if textView.text == "Description" {
        textView.text = nil
    }
}

func textViewDidEndEditing(_ textView: UITextView) {

    if textView.text.isEmpty {
        DispatchQueue.main.async {
            textView.text = "Description"
        }
    }
}
Taylor A. Leach
  • 2,115
  • 4
  • 25
  • 43
  • What do I type instead of last line ”textView.text = ”Description” if I want this value to be what user is typing ? – ISS Sep 06 '18 at 10:56
2

Contrary to just about every answer on this post, UITextView does have a placeholder property. For reasons beyond my comprehension, it is only exposed in IB, as such:

<userDefinedRuntimeAttributes>
  <userDefinedRuntimeAttribute type="string" keyPath="placeholder" value="My Placeholder"/>
</userDefinedRuntimeAttributes>

So if you are using storyboards and a static placeholder will suffice, just set the property on the inspector.

You can also set this property in code like this:

textView.setValue("My Placeholder", forKeyPath: "placeholder")

Its cloudy as to weather this is accessed via private API, as the property is exposed.

I haven't tried submitting with this method. But I will be submitting this way shortly and will update this answer accordingly.

UPDATE:

I have shipped this code in multiple releases with no issues from Apple.

UPDATE: This will only work in Xcode pre 11.2

Alex Chase
  • 960
  • 1
  • 7
  • 11
  • in Swift 5 you can write myTextView,placeholder = "enter your eye color" – user462990 Apr 19 '19 at 10:44
  • @user462990 Can you provide a documentation link for this? I don't believe this is accurate. – Alex Chase Apr 24 '19 at 20:09
  • sorry, didn't find dox but it's really easy to test... eg "alert.addTextField { (textField3) in textField3.placeholder = language.JBLocalize(phrase: "yourName") }. jbLocalize is a translation manager which returns a String – user462990 Apr 26 '19 at 12:00
  • 4
    @user462990 You are referencing `UITextField` NOT `UITextView` please read the questions/responses more carefully. – Alex Chase May 01 '19 at 00:22
  • @AlexChase Have you submitted your app and was it approved with the above solution. – George Jul 29 '19 at 08:46
  • @George yes! Sorry I forgot to report back. I have been using this code in production with no issues. – Alex Chase Jul 31 '19 at 19:39
  • 3
    Great solution, but does not work for me. Somehow it always crashes. Could you specify of deployment target, swift and xCode versions you are using? – T. Pasichnyk Aug 15 '19 at 12:20
  • @T.Pasichnyk so sorry to hear that. I'm not sure which version of UIKit this property was added in. It _should_ work if you can see the placeholder property on a textview within interfacebuilder. I am using Xcode 10.2.1 targeting iOS 11 – Alex Chase Aug 19 '19 at 03:51
  • In Xcode 11.2 – `*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key placeholder.'` – livingtech Nov 11 '19 at 18:38
  • @livingtech not surprised by this. Xcode 11.2 fixed a breaking error with UITextviews instantiated from a Xib. It could have broken this. I don't have Xcode 11.2 on my machine yet. Double check to see if placeholder text is still settable in IB on Xcode 11.2. I'll update my answer to exclude 11.2. Thanks for the info. – Alex Chase Nov 12 '19 at 00:56
2

Swift 4, 4.2 and 5

[![@IBOutlet var detailTextView: UITextView!

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

extension ContactUsViewController : UITextViewDelegate {

    public func textViewDidBeginEditing(_ textView: UITextView) {
        if textView.text == "Write your message here..." {
            detailTextView.text = ""
            detailTextView.textColor = UIColor.init(red: 0/255, green: 0/255, blue: 0/255, alpha: 0.86)

        }
        textView.becomeFirstResponder()
    }

    public func textViewDidEndEditing(_ textView: UITextView) {

        if textView.text == "" {
            detailTextView.text = "Write your message here..."
            detailTextView.textColor = UIColor.init(red: 0/255, green: 0/255, blue: 0/255, alpha: 0.30)
        }
        textView.resignFirstResponder()
    }
[![}][1]][1]
Akbar Khan
  • 2,215
  • 19
  • 27
2

Swift 5.2

Standalone class

Use this if you want a class which you can use anywhere as it is self contained

import UIKit
class PlaceHolderTextView:UITextView, UITextViewDelegate{
var placeholderText = "placeholderText"

override func willMove(toSuperview newSuperview: UIView?) {
    textColor = .lightText
    delegate = self
}

func textViewDidBeginEditing(_ textView: UITextView) {
    if textView.text == placeholderText{
        placeholderText = textView.text
        textView.text = ""
        textView.textColor = .darkText
    }
}

func textViewDidEndEditing(_ textView: UITextView) {
    if textView.text == ""{
        textView.text = placeholderText
        textColor = .lightText
    }
}    
}

The key here is the willMove(toSuperView:) function as it allows you to setup the view before being added to another view's hierarchy (similar to viewDidLoad/viewWillAppear in ViewControllers)

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
2

No need to add any third party library. Just use below code...

class SubmitReviewVC : UIViewController, UITextViewDelegate {

@IBOutlet var txtMessage : UITextView!
var lblPlaceHolder : UILabel!

override func viewDidLoad() {
    super.viewDidLoad()

    txtMessage.delegate = self
    lblPlaceHolder = UILabel()
    lblPlaceHolder.text = "Enter message..."
    lblPlaceHolder.font = UIFont.systemFont(ofSize: txtMessage.font!.pointSize)
    lblPlaceHolder.sizeToFit()
    txtMessage.addSubview(lblPlaceHolder)
    lblPlaceHolder.frame.origin = CGPoint(x: 5, y: (txtMessage.font?.pointSize)! / 2)
    lblPlaceHolder.textColor = UIColor.lightGray
    lblPlaceHolder.isHidden = !txtMessage.text.isEmpty
}

func textViewDidChange(_ textView: UITextView) {
    lblPlaceHolder.isHidden = !textView.text.isEmpty
}

}

Siddhesh Bhide
  • 374
  • 5
  • 9
1

There is no such property in ios to add placeholder directly in TextView rather you can add a label and show/hide on the change in textView. SWIFT 2.0 and make sure to implement the textviewdelegate

func textViewDidChange(TextView: UITextView)
{

 if  txtShortDescription.text == ""
    {
        self.lblShortDescription.hidden = false
    }
    else
    {
        self.lblShortDescription.hidden = true
    }

}
Himanshu
  • 2,832
  • 4
  • 23
  • 51
1

I like @nerdist's solution. Based on that, I created an extension to UITextView:

import Foundation
import UIKit

extension UITextView
{
  private func add(_ placeholder: UILabel) {
    for view in self.subviews {
        if let lbl = view as? UILabel  {
            if lbl.text == placeholder.text {
                lbl.removeFromSuperview()
            }
        }
    }
    self.addSubview(placeholder)
  }

  func addPlaceholder(_ placeholder: UILabel?) {
    if let ph = placeholder {
      ph.numberOfLines = 0  // support for multiple lines
      ph.font = UIFont.italicSystemFont(ofSize: (self.font?.pointSize)!)
      ph.sizeToFit()
      self.add(ph)
      ph.frame.origin = CGPoint(x: 5, y: (self.font?.pointSize)! / 2)
      ph.textColor = UIColor(white: 0, alpha: 0.3)
      updateVisibility(ph)
    }
  }

  func updateVisibility(_ placeHolder: UILabel?) {
    if let ph = placeHolder {
      ph.isHidden = !self.text.isEmpty
    }
  }
}

In a ViewController class, for example, this is how I use it:

class MyViewController: UIViewController, UITextViewDelegate {
  private var notePlaceholder: UILabel!
  @IBOutlet weak var txtNote: UITextView!
  ...
  // UIViewController
  override func viewDidLoad() {
    notePlaceholder = UILabel()
    notePlaceholder.text = "title\nsubtitle\nmore..."
    txtNote.addPlaceholder(notePlaceholder)
    ...
  }

  // UITextViewDelegate
  func textViewDidChange(_ textView: UITextView) {
    txtNote.updateVisbility(notePlaceholder)
    ...
  }

Placeholder on UITextview!

enter image description here

UPDATE:

In case you change textview's text in code, remember to call updateVisibitly method to hide placeholder:

txtNote.text = "something in code"
txtNote.updateVisibility(self.notePlaceholder) // hide placeholder if text is not empty.

To prevent the placeholder being added more than once, a private add() function is added in extension.

David.Chu.ca
  • 37,408
  • 63
  • 148
  • 190
  • Thanks again for your improvement to the original. I spent wayyy too much time on this this weekend, but I think you'll enjoy the EZ Placeholder extreme variant I eventually came up with: http://stackoverflow.com/a/41081244/2079103 *(I gave you credit for contributing to the evolution of the placeholder solutions)* – clearlight Dec 12 '16 at 17:16
1

In swift2.2:

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

}

In swift3:

import UIKit

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() {
    NotificationCenter.default.addObserver(self,
                                                     selector: #selector(textDidChange),
                                                     name: NSNotification.Name.UITextViewTextDidChange,
                                                     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
}

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

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

}

I wrote a class in swift. You need to import this class whenever required.

Sujatha Girijala
  • 1,141
  • 8
  • 20
1

I can't add comment because of reputation. add one more delegate need in @clearlight answer.

func textViewDidBeginEditing(_ textView: UITextView) { 
        cell.placeholderLabel.isHidden = !textView.text.isEmpty
}

is need

because textViewDidChange is not called first time

Kyle Yi
  • 448
  • 4
  • 11
1

no there is not any placeholder available for textview. you have to put label above it when user enter in textview then hide it or set by default value when user enters remove all values.

1

func setPlaceholder(){
var placeholderLabel = UILabel()
        placeholderLabel.text = "Describe your need..."
        placeholderLabel.font = UIFont.init(name: "Lato-Regular", size: 15.0) ?? UIFont.boldSystemFont(ofSize: 14.0)
        placeholderLabel.sizeToFit()
        descriptionTextView.addSubview(placeholderLabel)
        placeholderLabel.frame.origin = CGPoint(x: 5, y: (descriptionTextView.font?.pointSize)! / 2)
        placeholderLabel.textColor = UIColor.lightGray
        placeholderLabel.isHidden = !descriptionTextView.text.isEmpty
}



//Delegate Method.

func textViewDidChange(_ textView: UITextView) {
        placeholderLabel.isHidden = !textView.text.isEmpty
    }
 
Saurabh Sharma
  • 187
  • 2
  • 8
1

Another solution could be to use keyboardWillHide and keyboardWillShow notifications, as I did.

First you need to handle listening, and unlistening to the notifications in the viewWillAppear and viewWillAppear methods respectively (to handle memory leaks).

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    setupKeyboardNotificationListeners(enable: true)
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    setupKeyboardNotificationListeners(enable: false)
}

Then the method to handle listening/unlistening to the notifications:

private func setupKeyboardNotificationListeners(enable: Bool) {
        if enable {
            NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
            NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
        } else {
            NotificationCenter.default.removeObserver(self)
        }
    }

Then in the both methods for keyboardWillHide and keyboardWillShow you handle the placeholder and color changes of the text.

@objc func keyboardWillShow(notification: NSNotification) {
    if self.textView.text == self.placeholder {
        self.textView.text = ""
        self.textView.textColor = .black
    }
}

@objc func keyboardWillHide(notification: NSNotification) {
    if self.textView.text.isEmpty {
        self.textView.text = self.placeholder
        self.textView.textColor = .lightGrey
    }
}

I found this solution to be the best one so far since the text will be removed as soon as the keyboard appears instead of when the user starts typing, which can cause confusion.

nullforlife
  • 1,354
  • 2
  • 18
  • 30
1

I believe this is a very clean solution. It adds a dummy text view underneath the actual text view and shows or hides it depending on the text in the actual text view:

import Foundation
import UIKit

class TextViewWithPlaceholder: UITextView {

    private var placeholderTextView: UITextView = UITextView()

    var placeholder: String? {
        didSet {
            placeholderTextView.text = placeholder
        }
    }

    override var text: String! {
        didSet {
            placeholderTextView.isHidden = text.isEmpty == false
        }
    }

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

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

    private func commonInit() {
        applyCommonTextViewAttributes(to: self)
        configureMainTextView()
        addPlaceholderTextView()
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(textDidChange),
                                               name: UITextView.textDidChangeNotification,
                                               object: nil)
    }

    func addPlaceholderTextView() {
        applyCommonTextViewAttributes(to: placeholderTextView)
        configurePlaceholderTextView()
        insertSubview(placeholderTextView, at: 0)
    }

    private func applyCommonTextViewAttributes(to textView: UITextView) {
        textView.translatesAutoresizingMaskIntoConstraints = false
        textView.textContainer.lineFragmentPadding = 0
        textView.textContainerInset = UIEdgeInsets(top: 10,
                                                   left: 10,
                                                   bottom: 10,
                                                   right: 10)
    }

    private func configureMainTextView() {
        // Do any configuration of the actual text view here
    }

    private func configurePlaceholderTextView() {
        placeholderTextView.text = placeholder
        placeholderTextView.font = font
        placeholderTextView.textColor = UIColor.lightGray
        placeholderTextView.frame = bounds
        placeholderTextView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        placeholderTextView.frame = bounds
    }

    @objc func textDidChange() {
        placeholderTextView.isHidden = !text.isEmpty
    }

}
Adam Zarn
  • 1,868
  • 1
  • 16
  • 42
1

SWIFTUI

Here is a Swiftui TextView made using UIVIewRepresentable that has placeholder functionality and border colors

struct TextView: UIViewRepresentable {

@Binding var text: String
var placeholderText: String
var textStyle: UIFont.TextStyle

func makeUIView(context: Context) -> UITextView {
    let textView = UITextView()

    textView.font = UIFont.preferredFont(forTextStyle: textStyle)
    textView.autocapitalizationType = .sentences
    textView.isSelectable = true
    textView.isUserInteractionEnabled = true
    textView.delegate = context.coordinator
    textView.layer.borderWidth = 0.6
    textView.layer.borderColor = UIColor.lightGray.cgColor
    textView.layer.cornerRadius = 10
    textView.text = placeholderText
    textView.textColor = UIColor.lightGray
    return textView
}

func updateUIView(_ uiView: UITextView, context: Context) {
    uiView.text = text
    uiView.font = UIFont.preferredFont(forTextStyle: textStyle)
}

func makeCoordinator() -> Coordinator {
    Coordinator(self)
}
 
class Coordinator: NSObject, UITextViewDelegate {
    var parent: TextView
 
    init(_ parent: TextView) {
        self.parent = parent
    }
 
    func textViewDidChange(_ textView: UITextView) {
        self.parent.text = textView.text
    }
    
    func textViewDidBeginEditing(_ textView: UITextView) {
        if textView.textColor == UIColor.lightGray {
            textView.text = nil
            textView.textColor = UIColor.black
        }
    }
    
    func textViewDidEndEditing(_ textView: UITextView) {
        if textView.text.isEmpty {
            textView.text = self.parent.placeholderText
            textView.textColor = UIColor.lightGray
        }
    }
}

}

then in your View you can use it like this

TextView(text: self.$viewModel.addPostCommentText, placeholderText: "Share your story about this cash", textStyle: .body)
                .padding()
                .frame(height: 150)
Di Nerd Apps
  • 770
  • 8
  • 15
0

Protocol version of clearlight's answer above, because protocols are great. Pop in it where ever you please. Dunk!

extension UITextViewPlaceholder where Self: UIViewController {

    // Use this in ViewController's ViewDidLoad method.
    func addPlaceholder(text: String, toTextView: UITextView, font: UIFont? = nil) {
        placeholderLabel = UILabel()
        placeholderLabel.text = text
        placeholderLabel.font = font ?? UIFont.italicSystemFont(ofSize: (toTextView.font?.pointSize)!)
        placeholderLabel.sizeToFit()
        toTextView.addSubview(placeholderLabel)
        placeholderLabel.frame.origin = CGPoint(x: 5, y: (toTextView.font?.pointSize)! / 2)
        placeholderLabel.textColor = UIColor.lightGray
        placeholderLabel.isHidden = !toTextView.text.isEmpty
    }

    // Use this function in the ViewController's textViewDidChange delegate method.
    func textViewWithPlaceholderDidChange(_ textView: UITextView) {
        placeholderLabel.isHidden = !textView.text.isEmpty
    }
}
Alex Nguyen
  • 2,820
  • 1
  • 19
  • 14
0

TEXT VIEW DELEGATE METHODS

Use these two delegate methods and also write UITextViewDelegate in your class

func textViewDidBeginEditing(_ textView: UITextView) {
    if (commentsTextView.text == "Type Your Comments")
    {
        commentsTextView.text = nil
        commentsTextView.textColor = UIColor.darkGray
    }
}

func textViewDidEndEditing(_ textView: UITextView) {
    if commentsTextView.text.isEmpty
    {
        commentsTextView.text = "Type Your Comments"
        commentsTextView.textColor = UIColor.darkGray
    }
    textView.resignFirstResponder()
}
Community
  • 1
  • 1
Sai kumar Reddy
  • 1,751
  • 20
  • 23
0

Here's something that can be dropped into a UIStackView, it will size itself using an internal height constraint. Tweaking may be required to suit specific requirements.

import UIKit

public protocol PlaceholderTextViewDelegate: class {
  func placeholderTextViewTextChanged(_ textView: PlaceholderTextView, text: String)
}

public class PlaceholderTextView: UIView {

  public weak var delegate: PlaceholderTextViewDelegate?
  private var heightConstraint: NSLayoutConstraint?

  public override init(frame: CGRect) {
    self.allowsNewLines = true

    super.init(frame: frame)

    self.heightConstraint = self.heightAnchor.constraint(equalToConstant: 0)
    self.heightConstraint?.isActive = true

    self.addSubview(self.placeholderTextView)
    self.addSubview(self.textView)

    self.pinToCorners(self.placeholderTextView)
    self.pinToCorners(self.textView)

    self.updateHeight()
  }

  public override func didMoveToSuperview() {
    super.didMoveToSuperview()

    self.updateHeight()
  }

  private func pinToCorners(_ view: UIView) {
    NSLayoutConstraint.activate([
      view.leadingAnchor.constraint(equalTo: self.leadingAnchor),
      view.trailingAnchor.constraint(equalTo: self.trailingAnchor),
      view.topAnchor.constraint(equalTo: self.topAnchor),
      view.bottomAnchor.constraint(equalTo: self.bottomAnchor)
    ])
  }

  // Accessors
  public var text: String? {
    didSet {
      self.textView.text = text
      self.textViewDidChange(self.textView)
      self.updateHeight()
    }
  }

  public var textColor: UIColor? {
    didSet {
      self.textView.textColor = textColor
      self.updateHeight()
    }
  }

  public var font: UIFont? {
    didSet {
      self.textView.font = font
      self.placeholderTextView.font = font
      self.updateHeight()
    }
  }

  public override var tintColor: UIColor? {
    didSet {
      self.textView.tintColor = tintColor
      self.placeholderTextView.tintColor = tintColor
    }
  }

  public var placeholderText: String? {
    didSet {
      self.placeholderTextView.text = placeholderText
      self.updateHeight()
    }
  }

  public var placeholderTextColor: UIColor? {
    didSet {
      self.placeholderTextView.textColor = placeholderTextColor
      self.updateHeight()
    }
  }

  public var allowsNewLines: Bool

  public required init?(coder _: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  private lazy var textView: UITextView = self.newTextView()
  private lazy var placeholderTextView: UITextView = self.newTextView()

  private func newTextView() -> UITextView {
    let textView = UITextView()
    textView.translatesAutoresizingMaskIntoConstraints = false
    textView.isScrollEnabled = false
    textView.delegate = self
    textView.backgroundColor = .clear
    return textView
  }

  private func updateHeight() {
    let maxSize = CGSize(width: self.frame.size.width, height: .greatestFiniteMagnitude)

    let textViewSize = self.textView.sizeThatFits(maxSize)
    let placeholderSize = self.placeholderTextView.sizeThatFits(maxSize)

    let maxHeight = ceil(CGFloat.maximum(textViewSize.height, placeholderSize.height))

    self.heightConstraint?.constant = maxHeight
  }
}

extension PlaceholderTextView: UITextViewDelegate {
  public func textViewDidChangeSelection(_: UITextView) {
    self.placeholderTextView.alpha = self.textView.text.isEmpty ? 1 : 0
    self.updateHeight()
  }

  public func textViewDidChange(_: UITextView) {
    self.delegate?.placeholderTextViewTextChanged(self, text: self.textView.text)
  }

  public func textView(_: UITextView, shouldChangeTextIn _: NSRange,
                       replacementText text: String) -> Bool {
    let containsNewLines = text.rangeOfCharacter(from: .newlines)?.isEmpty == .some(false)
    guard !containsNewLines || self.allowsNewLines else { return false }

    return true
  }
}
jwswart
  • 1,226
  • 14
  • 16
0
var placeholderLabel : UILabel!
  textviewDescription.delegate = self
    placeholderLabel = UILabel()
    placeholderLabel.text = "Add a description"

   func textViewDidChange(_ textView: UITextView) {
             placeholderLabel.isHidden = !textviewDescription.text.isEmpty
    }
Srinivasan.M
  • 257
  • 2
  • 4
  • Textview placeholder to use only label for placeholder in textview and hidden the placeholder to enter the text in textview – Srinivasan.M Sep 19 '18 at 07:46
0

Our solution avoids mucking with the UITextView text and textColor properties, which is handy if you're maintaining a character counter.

It's simple:

1) Create a dummy UITextView in Storyboard with the same properties as the master UITextView. Assign placeholder text to the dummy text.

2) Align the top, left, and right edges of the two UITextViews.

3) Place the dummy behind the master.

4) Override the textViewDidChange(textView:) delegate function of the master, and show the dummy if the master has 0 characters. Otherwise, show the master.

This assumes both UITextViews have transparent backgrounds. If they do not, place the dummy on top when there are 0 characters, and push it underneath when there are > 0 characters. You will also have to swap responders to make sure the cursor follows the right UITextView.

Crashalot
  • 33,605
  • 61
  • 269
  • 439
0

there is my simple version of UITextView with placeholder. The main idea is:

  • hide placeholder if user starts editing and placeholder is visible
  • show placeholder if user ends editing and text of text view is empty.
class PlaceholderTextView: UITextView {

    var placeholder = "" {
        didSet {
            if isPlaceholderVisible {
                showPlaceholder()
            }
        }
    }

    var isPlaceholderVisible = true {
        didSet {
            isPlaceholderVisible ? showPlaceholder() : hidePlaceholder()
        }
    }

    init() {
        super.init(frame: .zero, textContainer: nil)
        delegate = self
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func showPlaceholder() {
        text = placeholder
        // Set other things like color of text for placeholder, ...
    }

    private func hidePlaceholder() {
        text = ""
        // Set other things like color of text for normal input, ...
    }

}

extension PlaceholderTextView: UITextViewDelegate {
    func textViewDidBeginEditing(_ textView: UITextView) {
        if isPlaceholderVisible {
            isPlaceholderVisible = false
        }
    }

    func textViewDidEndEditing(_ textView: UITextView) {
        if text.isEmpty {
            isPlaceholderVisible = true
        }
    }
}
Robert Dresler
  • 10,580
  • 2
  • 22
  • 40
0

This is what I did. Leaning towards code clarity and simplicity. I needed to add a textView that will get some additional notes on my app. This additional notes can be created or amended after being saved. See below. HTH. :)

class NotesTextView: UITextView {

    var placeholder = "" {
        didSet {
            showPlaceholder()
        }
    }
    
    // if the text is the placeholder, then assign a color fitting for a
    // placeholder text, else, assign it your color of choice.
    override var text: String! {
        didSet {
            textColor = text == placeholder ? .tertiaryLabel : .systemBlue
        }
    }
    
    
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        
        delegate = self
        //config your font and translateAutoResizingMaskIntoConstraints here
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func showPlaceholder() {
        text = placeholder
    }
    
    private func hidePlaceholder() {
        text = ""
    }
}

extension NotesTextView: UITextViewDelegate {
    func textViewDidBeginEditing(_ textView: UITextView) {
        if text == placeholder {
            hidePlaceholder()
        }
    }
    
    func textViewDidEndEditing(_ textView: UITextView) {
        if text.isEmpty {
            showPlaceholder()
        }
    }
}
arvinq
  • 656
  • 6
  • 12
0
import UIKit
import RxSwift

@IBDesignable class TextViewWithPlaceholder: UITextView {
    
    //MARK: - Propertise
    @IBInspectable var placeholderText: String = ""
    
    let placeholderLabel = LocalizedUILabel()
    private let hidePlaceholderObserver = PublishSubject<Bool>()
    let disposeBag = DisposeBag()
    
    
    //MARK: - Did Move To Window
    override func didMoveToWindow() {
        super.didMoveToWindow()
        observeOnTextViewEditing()
        configurePlaceholder()
    }
    
    
    //MARK: - Observe On Text View Editing
    private func observeOnTextViewEditing() {
        rx.text.subscribe(onNext: { [weak self] selectedText in
            guard let self = self else { return }
            self.hidePlaceholderObserver.onNext((selectedText?.isEmpty ?? true) ? false : true)
            
        }).disposed(by: disposeBag)
    }
    
    
    //MARK: - Observe On Show Hide Placeholder
    private func configurePlaceholder() {
        hidePlaceholderObserver
            .bind(to: placeholderLabel.rx.isHidden)
            .disposed(by: disposeBag)
        
        placeholderLabel.text = placeholderText
        placeholderLabel.font = UIFont(name: "Poppins-Semibold", size: 16) ?? UIFont()
        placeholderLabel.textColor = .lightGray
        
        placeholderLabel.sizeToFit()
        placeholderLabel.frame.origin = CGPoint(x: 8, y: 8)
        addSubview(placeholderLabel)
    }
    
}
Ranoiaetep
  • 5,872
  • 1
  • 14
  • 39
Andrew Ehab
  • 1
  • 1
  • 1
0

Probably the most simple out-of-the-box solution for UITextView placeholder implementation that does not suffer from:

  • using UILabel instead of UITextView that might perform differently
  • switching to and from placeholder 'UITextView' copy that would capture first typed character that will miss from main UITextView control
  • messing with main UITextView controls text content replacing placeholder with empty string or first typed character. Border case is that if user enters placeholder text, some proposed implementation will treat it as a placeholder itself.

Swift 5:

import UIKit
import SnapKit
import RxSwift
import RxCocoa

class TextAreaView: UIView {
    let textArea = UITextView()
    let textAreaPlaceholder = UITextView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonSetup()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonSetup()
    }
    
    private func commonSetup() {
        
        addSubview(textAreaPlaceholder)
        addSubview(textArea)
        
        textArea.isScrollEnabled = false
        textArea.delegate = self

        textAreaPlaceholder.isScrollEnabled = false
        textAreaPlaceholder.textColor = UIColor.lightGray
        
        textArea.snp.makeConstraints { make in
            make.top.bottom.leading.trailing.equalToSuperview()
        }

        textAreaPlaceholder.snp.makeConstraints { make in
            make.top.bottom.leading.trailing.equalTo(textArea.snp.top)
        }

        textAreaPlaceholder.text = "Placeholder"
        
        updatePlaceholder()
    }
    
    func updatePlaceholder() {
        if textArea.text.count > 0 {
            textArea.alpha = 1.0
        } else {
            textArea.alpha = 0.0
        }
    }
}

extension TextAreaView: UITextViewDelegate {
    func textViewDidChange(_ textView: UITextView) {
        updatePlaceholder()
    }
}
vedrano
  • 2,961
  • 1
  • 28
  • 25
-2

I know this is a an old question but wanted to share what I thought was a useful way of extending UITextView to have placeholderText and placeholderColor fields. Basically you cast the UITextView into a UITextField and then set the attributedPlaceholder field. PlaceholderText and placeholderColor are IBInspectable fields, so their values can be set in IB and behaves exactly as the UITextField placeholder functionality.

UITextView+Extend.h

#import <UIKit/UIKit.h>

@interface UITextView (Extend)

@property (nonatomic) IBInspectable NSString *placeholderText;
@property (nonatomic) IBInspectable UIColor *placeholderColor;

@end

UITextView+Extend.m

#import "UITextView+Extend.h"
#import "objc/runtime.h"

@implementation UITextView (Extend)

- (void)setPlaceholderText:(NSString *)placeholderText
{
    objc_setAssociatedObject(self, @selector(placeholderText), placeholderText, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    [self updatePlaceholderText];
}

- (NSString*)placeholderText
{
    return objc_getAssociatedObject(self, @selector(placeholderText));
}

- (void)setPlaceholderColor:(UIColor *)placeholderColor
{
    objc_setAssociatedObject(self, @selector(placeholderColor), placeholderColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    [self updatePlaceholderText];
}

- (UIColor*)placeholderColor
{
    return objc_getAssociatedObject(self, @selector(placeholderColor));
}

- (void)updatePlaceholderText
{
    NSString *text = self.placeholderText;
    UIColor *color = self.placeholderColor;
    if(text && color)
    {
        UITextField *textField = (UITextField*)self;
        textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:text attributes:@{NSForegroundColorAttributeName:color}];
    }
}

@end
Feta
  • 522
  • 1
  • 5
  • 11