Как добавить текст-заполнитель (placeholder) в UITextView в Swift?
Правильная реализация 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)
}
}
Как использовать
- В Storyboard: Установите класс вашего
UITextViewкакPlaceholderTextViewв Identity Inspector. Затем в Attributes Inspector задайтеplaceholderText(например, "Введите заметку...") и изменитеplaceholderColorпри необходимости. - В коде:
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, чтобы не
Ответов пока нет.
Войдите чтобы оставить ответ