我的应用程序使用UITextView。现在我想让UITextView有一个占位符,类似于你可以为UITextField设置的占位符。

如何做到这一点?


当前回答

简单地创建UITextView的@IBDesignable子类:

@IBDesignable class AttributedTextView: UITextView {

    private let placeholderLabel = UILabel()

    @IBInspectable var placeholder: String = "" {

        didSet {

            setupPlaceholderLabelIfNeeded()
            textViewDidChange()
        }
    }

    override var text: String! {

        didSet {
            textViewDidChange()
        }
    }

    //MARK: - Initialization

    override func awakeFromNib() {
        super.awakeFromNib()

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

    //MARK: - Deinitialization

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    //MARK: - Internal

    func textViewDidChange() {

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

    //MARK: - Private

    private func setupPlaceholderLabelIfNeeded() {

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

        placeholderLabel.sizeToFit()

        insertSubview(placeholderLabel, at: 0)
    }
}

然后在标识检查器中设置占位符:

其他回答

我建议使用SZTextView。

https://github.com/glaszig/SZTextView

从storyboard中添加默认的UITextView,然后将其自定义类更改为SZTextView,如下所示

然后您将在属性检查器中看到两个新选项


模拟本机占位符


一个常见的抱怨是iOS没有为文本视图提供原生占位符功能。下面的UITextView扩展试图通过提供人们期望从本机特性获得的便利来解决这个问题,只需要一行代码就可以向textview实例添加占位符。

这个解决方案的缺点是,因为它菊花链委托调用,它很容易(不太可能)在iOS更新中更改UITextViewDelegate协议。具体来说,如果iOS添加了新的协议方法,并且你在带有占位符的文本视图的委托中实现了它们中的任何一个,这些方法将不会被调用,除非你也更新了扩展来转发这些调用。

或者,内联占位符的答案是一个坚如磐石的和简单的可以。


使用例子:


•如果获得占位符的文本视图没有使用UITextViewDelegate:

    /* Swift 3 */

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

                                            -- 或者——

•如果获得占位符的文本视图使用了UITextViewDelegate:

    /* Swift 3 */

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

实现(UITextView扩展):


/* Swift 3 */

extension UITextView: UITextViewDelegate
{

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

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

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

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

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

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

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

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

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

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

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

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

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

1. 作为一个像UITextView这样的基本iOS类的扩展,重要的是要知道这段代码没有与任何不激活占位符的textview交互,例如textview实例没有被调用addPlaceholder()初始化

2. 支持占位符的文本视图透明地变成一个UITextViewDelegate来跟踪字符计数,以控制占位符可见性。如果一个委托被传递给addPlaceholder(),这段代码会将委托回调给该委托。

3.作者正在研究如何检查UITextViewDelegate协议并自动代理它,而不必硬编码每个方法。这将使代码免受方法签名更改和添加到协议中的新方法的影响。

我做了我自己版本的UITextView的子类。我喜欢Sam Soffes使用通知的想法,但我不喜欢drawRect: overwrite。对我来说太过分了。我认为我做了一个非常干净的实现。

你可以看看我的子类。还包括一个演示项目。

在看完所有答案后,我意识到我需要一些简单的,同时可重用的东西,这样我就可以在我的项目中为所有uitextview添加相同的功能。最后我得到了以下代码:

extension UITextView {

// MARK: TextView PlaceHolderLabel Setup

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

// Add your constraints here

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

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

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

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

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

}

当UILabel作为子视图添加到UITextView时,textView游标位置和UILabel位置将不匹配,因此在占位符文本之前留下一个空间

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

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

然后在textViewdidChange方法中添加以下代码

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

它可以在所有uitextview中重用。

Jason的答案在iOS7中看起来有点不对劲,通过调整_placeHolderLabel的偏移量来修复它:

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

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

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

    [super drawRect:rect];
}