简体   繁体   中英

Placeholder text inside UITextView in Swift

I have done some research and found this post: Add placeholder text inside UITextView in Swift?

Incorporating the above example in my code, I have the following in a blank xcode UIKit project:

import UIKit

class ViewController: UIViewController {

    var sampleTextView       = UITextView()
    let placeholderText      = "Type Something"
    let placeholderTextColor = UIColor.lightGray
    let normalTextColor      = UIColor.label
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemGray
        
        sampleTextView.delegate     = self
        sampleTextView.text         = placeholderText
        sampleTextView.textColor    = placeholderTextColor
        sampleTextView.becomeFirstResponder()
        sampleTextView.selectedTextRange = sampleTextView.textRange(from: sampleTextView.beginningOfDocument, to: sampleTextView.beginningOfDocument)

        view.addSubview(sampleTextView)
        sampleTextView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            sampleTextView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0),
            sampleTextView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
            sampleTextView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
            sampleTextView.heightAnchor.constraint(equalToConstant: 100)
        ])
    }
}

extension ViewController: UITextViewDelegate {
    
    func textViewDidEndEditing(_ textView: UITextView) {
        if textView.text.isEmpty {
            textView.text = placeholderText
            textView.textColor = placeholderTextColor
        }
    }
    
    
    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 = placeholderText
            textView.textColor = placeholderTextColor

            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 == placeholderTextColor && !text.isEmpty {
            textView.textColor = normalTextColor
            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
    }
    
    
    func textViewDidChangeSelection(_ textView: UITextView) {
        if self.view.window != nil {
            if textView.textColor == placeholderTextColor {
                textView.selectedTextRange = textView.textRange(from: textView.beginningOfDocument, to: textView.beginningOfDocument)
            }
        }
    }
}

But I have two bugs with this that for the life of me, I can't work out:

  1. This first is using the predictive text above the keyboard toolbar. The first selected work is repeated - see screenshot below:

在此处输入图片说明

  1. If typing, the keyboard's SHIFT key is pressed as expected for the first letter, but then stays pressed for the second one as well - see screenshot below:

在此处输入图片说明

Apologies if these are basic but I'm stumped.

Couple notes...

I think the Shift is not being released because you return False from shouldChangeTextIn range ... so the textView doesn't fully communicate with the keyboard.

As to the duplicated predictive text... I've run across similar issues. My impression is that the text is already inserted and a new .selectedRange is set by the time shouldChangeTextIn range is called. So the code (as written) duplicates it.

Because of those (and other) complications, here's an example of using a CATextLayer as the "placeholder" text:

class PlaceHolderTestViewController: UIViewController, UITextViewDelegate {

    var sampleTextView       = UITextView()
    
    let placeholderText      = "Type Something"
    let placeholderTextColor = UIColor.lightGray
    let normalTextColor      = UIColor.label
    
    let textLayer = CATextLayer()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemGray
        
        if self.navigationController != nil {
            // add a "Done" navigation bar button
            let btn = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTap))
            self.navigationItem.rightBarButtonItem = btn
        }
        
        view.addSubview(sampleTextView)
        sampleTextView.translatesAutoresizingMaskIntoConstraints = false
        
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            sampleTextView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40),
            sampleTextView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 10),
            sampleTextView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -10),
            sampleTextView.heightAnchor.constraint(equalToConstant: 100)
        ])

        sampleTextView.font = .systemFont(ofSize: 16.0)

        // textLayer properties
        textLayer.contentsScale = UIScreen.main.scale
        textLayer.alignmentMode = .left
        textLayer.isWrapped = true
        textLayer.foregroundColor = placeholderTextColor.cgColor
        textLayer.string = placeholderText
    
        if let fnt = sampleTextView.font {
            textLayer.fontSize = fnt.pointSize
        } else {
            textLayer.fontSize = 12.0
        }

        // insert the textLayer
        sampleTextView.layer.insertSublayer(textLayer, at: 0)

        // set delegate to self
        sampleTextView.delegate = self
    }
    
    func textViewDidChange(_ textView: UITextView) {
        textLayer.opacity = textView.text.isEmpty ? 1.0 : 0.0
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        // update textLayer frame here
        textLayer.frame = sampleTextView.bounds.insetBy(
            dx: sampleTextView.textContainerInset.left + sampleTextView.textContainer.lineFragmentPadding,
            dy: sampleTextView.textContainerInset.top
        )
    }
    
    @objc func doneTap() -> Void {
        view.endEditing(true)
    }
    
}

and, here's an example of subclassing UITextView to make it easier to reuse -- as well as easily allowing more than one textView at a time:

class PlaceholderTextView: UITextView, UITextViewDelegate {
    
    private let textLayer = CATextLayer()
    
    public var placeholderText: String = "" {
        didSet {
            textLayer.string = placeholderText
            setNeedsLayout()
        }
    }
    public var placeholderTextColor: UIColor = .lightGray {
        didSet {
            textLayer.foregroundColor = placeholderTextColor.cgColor
            setNeedsLayout()
        }
    }
    override var font: UIFont? {
        didSet {
            if let fnt = self.font {
                textLayer.fontSize = fnt.pointSize
            } else {
                textLayer.fontSize = 12.0
            }
        }
    }
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() -> Void {
        // textLayer properties
        textLayer.contentsScale = UIScreen.main.scale
        textLayer.alignmentMode = .left
        textLayer.isWrapped = true
        textLayer.foregroundColor = placeholderTextColor.cgColor

        if let fnt = self.font {
            textLayer.fontSize = fnt.pointSize
        } else {
            textLayer.fontSize = 12.0
        }

        // insert the textLayer
        layer.insertSublayer(textLayer, at: 0)

        // set delegate to self
        delegate = self
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        textLayer.frame = bounds.insetBy(
            dx: textContainerInset.left + textContainer.lineFragmentPadding,
            dy: textContainerInset.top
        )
    }
    func textViewDidChange(_ textView: UITextView) {
        // show / hide the textLayer
        textLayer.opacity = textView.text.isEmpty ? 1.0 : 0.0
    }
}

class PlaceHolderTestViewController: UIViewController {

    let sampleTextView = PlaceholderTextView()

    let placeholderText      = "Type Something"
    let placeholderTextColor = UIColor.lightGray
    let normalTextColor      = UIColor.label

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemGray
        
        if self.navigationController != nil {
            // add a "Done" navigation bar button
            let btn = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTap))
            self.navigationItem.rightBarButtonItem = btn
        }

        view.addSubview(sampleTextView)
        sampleTextView.translatesAutoresizingMaskIntoConstraints = false

        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            sampleTextView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40),
            sampleTextView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 10),
            sampleTextView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -10),
            sampleTextView.heightAnchor.constraint(equalToConstant: 100)
        ])

        sampleTextView.font = .systemFont(ofSize: 16.0)
        sampleTextView.textColor = normalTextColor
        sampleTextView.placeholderTextColor = placeholderTextColor
        sampleTextView.placeholderText = placeholderText

    }
    
    @objc func doneTap() -> Void {
        view.endEditing(true)
    }

}

Note -- this is Example Code Only and should not be considered "Production Ready."

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM