Почему UITextView не имеет встроенного placeholder и как это обойти
Каждый iOS-разработчик, который хоть раз работал с UITextView, сталкивался с одной и той же проблемой: в отличие от UITextField, у UITextView нет встроенного свойства placeholder. Вы создаёте многострочное поле для ввода текста, запускаете приложение — и видите пустой белый прямоугольник. Пользователь не понимает, что туда нужно вводить, а дизайнер негодует.
Почему Apple не добавила это свойство? Исторически UITextView позиционировался как элемент для отображения и редактирования больших объёмов текста (например, заметок или писем), а не как поле ввода с подсказкой. Но времена изменились: сегодня мы используем UITextView для комментариев, описаний, биографий — везде, где нужен placeholder.
В этой статье мы разберём 3 проверенных способа добавить placeholder text inside UITextView in Swift. Вы узнаете, как сделать это через UILabel, через атрибуты NSAttributedString и через создание собственного подкласса. Поехали!
Способ 1: Классический UILabel поверх UITextView
Самый простой и понятный способ — разместить UILabel поверх UITextView и управлять его видимостью. Этот метод идеален для новичков и для проектов, где не требуется сложная кастомизация.
Как это работает
Вы добавляете UILabel как дочерний элемент UITextView, задаёте ему текст placeholder и скрываете, когда пользователь начинает печатать. Вот пошаговый пример:
import UIKit
class ViewController: UIViewController, UITextViewDelegate {
@IBOutlet weak var textView: UITextView!
let placeholderLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
setupPlaceholder()
}
func setupPlaceholder() {
placeholderLabel.text = "Введите ваш комментарий..."
placeholderLabel.font = UIFont.systemFont(ofSize: 16)
placeholderLabel.textColor = UIColor.lightGray
placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
textView.addSubview(placeholderLabel)
NSLayoutConstraint.activate([
placeholderLabel.topAnchor.constraint(equalTo: textView.topAnchor, constant: 8),
placeholderLabel.leadingAnchor.constraint(equalTo: textView.leadingAnchor, constant: 5),
placeholderLabel.trailingAnchor.constraint(equalTo: textView.trailingAnchor, constant: -5)
])
}
func textViewDidChange(_ textView: UITextView) {
placeholderLabel.isHidden = !textView.text.isEmpty
}
}
Важно: не забудьте установить делегат UITextViewDelegate и реализовать метод textViewDidChange. Если вы используете SwiftUI, подход будет другим — там используется модификатор .overlay.
Плюсы и минусы
- Плюсы: простота, легкость в понимании, работает на любой версии iOS.
- Минусы: нужно следить за констрейнтами, если у textview меняются insets. Также не сработает, если textview имеет
NSAttributedStringс разными выравниваниями.
Способ 2: Placeholder через NSAttributedString
Более элегантный способ — использовать атрибуты строки. Идея в том, что вы показываете placeholder как атрибутированный текст с серым цветом, а когда пользователь начинает ввод — заменяете его на обычный текст.
Этот метод особенно полезен, если вы работаете с UITextView в Swift и не хотите заморачиваться с дополнительными вьюхами.
Реализация
import UIKit
class ViewController: UIViewController, UITextViewDelegate {
@IBOutlet weak var textView: UITextView!
let placeholderText = "Напишите что-нибудь..."
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
showPlaceholder()
}
func showPlaceholder() {
textView.text = placeholderText
textView.textColor = UIColor.lightGray
}
func textViewDidBeginEditing(_ textView: UITextView) {
if textView.text == placeholderText && textView.textColor == UIColor.lightGray {
textView.text = ""
textView.textColor = UIColor.black
}
}
func textViewDidEndEditing(_ textView: UITextView) {
if textView.text.isEmpty {
showPlaceholder()
}
}
}
Обратите внимание: мы проверяем не только текст, но и цвет. Почему? Потому что пользователь мог ввести точно такой же текст, как placeholder — и тогда мы бы ошибочно очистили поле.
Ловушка: что если пользователь скопирует текст placeholder?
Да, это реальный кейс. Если пользователь введёт слово "Напишите что-нибудь..." чёрным цветом, наш код решит, что это placeholder. Решение: используйте отдельную булеву переменную isShowingPlaceholder для отслеживания состояния.
Способ 3: Создание подкласса UITextView с placeholder
Для продакшн-проектов лучше всего создать собственный подкласс UITextView. Это даёт максимальную гибкость и переиспользуемость. Вы один раз пишете класс, а потом используете его во всём проекте.
Полный код подкласса
import UIKit
class PlaceholderTextView: UITextView {
var placeholder: String = "Placeholder" {
didSet {
placeholderLabel.text = placeholder
setNeedsLayout()
}
}
var placeholderColor: UIColor = UIColor.lightGray {
didSet {
placeholderLabel.textColor = placeholderColor
}
}
private let placeholderLabel = UILabel()
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
placeholderLabel.text = placeholder
placeholderLabel.textColor = placeholderColor
placeholderLabel.font = font ?? UIFont.systemFont(ofSize: 16)
placeholderLabel.numberOfLines = 0
placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(placeholderLabel)
NSLayoutConstraint.activate([
placeholderLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8),
placeholderLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5),
placeholderLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5)
])
NotificationCenter.default.addObserver(self,
selector: #selector(textChanged),
name: UITextView.textDidChangeNotification,
object: nil)
}
@objc private func textChanged() {
placeholderLabel.isHidden = !text.isEmpty
}
override var text: String! {
didSet {
textChanged()
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
Как использовать в коде
let textView = PlaceholderTextView()
textView.placeholder = "Ваше сообщение..."
textView.placeholderColor = UIColor.systemGray3
textView.frame = CGRect(x: 20, y: 100, width: 300, height: 150)
view.addSubview(textView)