Swift 中 UITextView 内的占位符文本

Placeholder text inside UITextView in Swift

我做了一些研究,发现了这个 post:Add placeholder text inside UITextView in Swift?

将上面的示例合并到我的代码中,我在空白 xcode UIKit 项目中有以下内容:

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

但我有两个错误,我无法解决这个问题:

  1. 这首先是使用键盘工具栏上方的预测文本。重复第一个选定的作品 - 请参见下面的屏幕截图:

  1. 如果键入,第一个字母按预期按下键盘的 SHIFT 键,但第二个字母也保持按下状态 - 请参见下面的屏幕截图:

抱歉,如果这些是基本的,但我很难过。

情侣笔记...

我认为 Shift 没有被释放,因为你 return False 来自 shouldChangeTextIn range ...所以 textView 没有完全与键盘沟通。

至于重复的预测文本...我 运行 遇到过类似的问题。我的印象是文本 已经插入 并且在调用 shouldChangeTextIn range 时设置了新的 .selectedRange。所以代码(如所写)复制了它。

由于这些(和其他)复杂性,这里有一个使用 CATextLayer 作为“占位符”文本的示例:

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

并且,这里有一个子类化 UITextView 的示例,以使其更易于重用——以及轻松地一次允许多个 textView:

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

}

注意 -- 这只是 示例代码 不应被视为“生产准备好了。