Avatar ·

Как добавить текст-заполнитель (placeholder) в UITextView в Swift?

Всем привет! Я новичок в Swift и iOS-разработке. Я пытаюсь создать экран для ввода текста, похожий на заметки в iOS, где в UITextView есть серый текст-подсказка "Введите текст...", который исчезает, когда пользователь начинает печатать, и появляется снова, если поле пустое. В UITextField есть свойство `placeholder`, которое делает это автоматически, но в UITextView такого свойства нет. Я пробовал использовать `UITextViewDelegate` и метод `textViewDidChange`, чтобы вручную управлять отображением текста-заполнителя, но у меня возникли проблемы: 1. Когда я устанавливаю текст-заполнитель через `textView.text = "Введите текст..."`, он не исчезает, когда пользователь начинает печатать — текст просто дописывается к placeholder. 2. Если я пытаюсь очищать текст в `textViewDidBeginEditing`, то теряю возможность редактирования, если пользователь случайно коснулся поля, а потом ушел. 3. Я не могу понять, как правильно управлять цветом текста: placeholder должен быть серым, а введенный пользователем текст — черным. Вот мой текущий код, который не работает: ```swift class ViewController: UIViewController, UITextViewDelegate { @IBOutlet weak var textView: UITextView! override func viewDidLoad() { super.viewDidLoad() textView.delegate = self textView.text = "Введите текст..." textView.textColor = UIColor.lightGray } func textViewDidBeginEditing(_ textView: UITextView) { if textView.text == "Введите текст..." { textView.text = "" textView.textColor = UIColor.black } } func textViewDidEndEditing(_ textView: UITextView) { if textView.text == "" { textView.text = "Введите текст..." textView.textColor = UIColor.lightGray } } } ``` Проблема в том, что если пользователь набирает текст, а потом удаляет его и уходит, placeholder не появляется, потому что я проверяю только пустую строку. Также, если пользователь вводит текст, а потом выходит и заходит снова, текст сбрасывается. Как правильно реализовать placeholder для UITextView? Заранее спасибо! <<>>

Правильная реализация placeholder для UITextView в Swift

Ваша основная ошибка — в том, что вы используете сам UITextView для отображения placeholder, что создает конфликт с вводимым текстом. Лучший подход — добавить отдельную UILabel поверх UITextView и управлять ее видимостью. Это надежный и чистый способ, который не нарушает логику редактирования.

Пошаговое решение

Мы создадим кастомный класс PlaceholderTextView, который наследуется от UITextView и автоматически управляет placeholder. Это решение переиспользуемо и работает как с кодом, так и со Storyboard.

import UIKit

class PlaceholderTextView: UITextView {
    
    // MARK: - Свойства
    private let placeholderLabel = UILabel()
    
    @IBInspectable var placeholderText: String? {
        didSet { placeholderLabel.text = placeholderText }
    }
    
    @IBInspectable var placeholderColor: UIColor = .lightGray {
        didSet { placeholderLabel.textColor = placeholderColor }
    }
    
    override var text: String! {
        didSet { updatePlaceholderVisibility() }
    }
    
    override var font: UIFont? {
        didSet {
            placeholderLabel.font = font
            // Обновляем размеры метки при изменении шрифта
            setNeedsLayout()
        }
    }
    
    override var textAlignment: NSTextAlignment {
        didSet { placeholderLabel.textAlignment = textAlignment }
    }
    
    // MARK: - Инициализация
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        setupPlaceholder()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupPlaceholder()
    }
    
    private func setupPlaceholder() {
        // Настройка метки
        placeholderLabel.textColor = placeholderColor
        placeholderLabel.numberOfLines = 0
        placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
        placeholderLabel.isUserInteractionEnabled = false
        placeholderLabel.font = self.font ?? UIFont.systemFont(ofSize: 17)
        
        addSubview(placeholderLabel)
        
        // Констрейнты: отступы как у UITextView
        NSLayoutConstraint.activate([
            placeholderLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: textContainerInset.top + 8),
            placeholderLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: textContainerInset.left + 5),
            placeholderLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -(textContainerInset.right + 5)),
            placeholderLabel.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor, constant: -(textContainerInset.bottom + 8))
        ])
        
        // Слушаем изменения текста через NotificationCenter
        NotificationCenter.default.addObserver(self, selector: #selector(textDidChange), name: UITextView.textDidChangeNotification, object: self)
        
        // Начальная видимость
        updatePlaceholderVisibility()
    }
    
    // MARK: - Обновление видимости
    @objc private func textDidChange() {
        updatePlaceholderVisibility()
    }
    
    private func updatePlaceholderVisibility() {
        placeholderLabel.isHidden = !self.text.isEmpty
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

Как использовать

  1. В Storyboard: Установите класс вашего UITextView как PlaceholderTextView в Identity Inspector. Затем в Attributes Inspector задайте placeholderText (например, "Введите заметку...") и измените placeholderColor при необходимости.
  2. В коде:
let textView = PlaceholderTextView()
textView.placeholderText = "Напишите что-нибудь..."
textView.placeholderColor = UIColor.systemGray3
textView.frame = CGRect(x: 20, y: 100, width: 300, height: 200)
view.addSubview(textView)

Почему это работает

  • Разделение ответственности: placeholder — это отдельная UILabel, а не текст UITextView. Поэтому не возникает конфликтов при редактировании.
  • Автоматическое обновление: Используется NotificationCenter для отслеживания изменений текста. Это проще и надежнее, чем делегат, так как не требует настройки делегата.
  • Гибкость: Вы можете настроить цвет, шрифт, отступы и даже анимацию появления/исчезновения placeholder.
  • Совместимость: Работает с Auto Layout и адаптируется к изменению размера текстового поля.

Дополнительные улучшения

Если хотите добавить анимацию появления/исчезновения, измените метод updatePlaceholderVisibility:

private func updatePlaceholderVisibility() {
    UIView.animate(withDuration: 0.2) {
        self.placeholderLabel.alpha = self.text.isEmpty ? 1.0 : 0.0
    }
}

Также можно добавить поддержку UITextInputTraits (например, для пароля или email) — просто унаследуйте класс и настройте свойства.

<<>>

Альтернативный подход: использование attributedText и делегата

Если вы не хотите создавать отдельный класс, можно реализовать placeholder через делегат UITextViewDelegate и атрибутированный текст. Этот метод проще, но требует аккуратности, чтобы не потерять данные пользователя.

class ViewController: UIViewController, UITextViewDelegate {
    @IBOutlet weak var textView: UITextView!
    
    private let placeholder = "Введите текст..."
    private var isPlaceholderActive = true
    
    override func viewDidLoad() {
        super.viewDidLoad()
        textView.delegate = self
        showPlaceholder()
    }
    
    private func showPlaceholder() {
        isPlaceholderActive = true
        textView.text = placeholder
        textView.textColor = UIColor.lightGray
    }
    
    private func hidePlaceholder() {
        isPlaceholderActive = false
        textView.text = ""
        textView.textColor = UIColor.black
    }
    
    // MARK: - UITextViewDelegate
    func textViewDidBeginEditing(_ textView: UITextView) {
        if isPlaceholderActive {
            hidePlaceholder()
        }
    }
    
    func textViewDidEndEditing(_ textView: UITextView) {
        if textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            showPlaceholder()
        }
    }
    
    func textViewDidChange(_ textView: UITextView) {
        // Если пользователь удалил весь текст и поле пустое — показываем placeholder
        if textView.text.isEmpty && !isPlaceholderActive {
            showPlaceholder()
            textView.resignFirstResponder() // Опционально: скрыть клавиатуру
        }
    }
}

Особенности этого подхода

  • Плюсы: Не требует создания подкласса, легко встраивается в существующий код.
  • Минусы: При переключении между placeholder и вводом текста происходит замена текста, что может сбросить выделение и позицию курсора. Также нужно быть осторожным с методом textViewDidChange, чтобы не