It is unfortunate that both UITextView
and UITextField
are so limited. These same things have been done for so many times by so many developers.
What I usually do is just create a new UIView
subclass which then has 2 subviews; A label for placeholder and centered text view for content.
You can do it in xib and use the interface builder and I suggest you to do so. You can then expose both text view and placeholder and you may actually set pretty much anything you would ever need.
But to do it in the code it would look something like the following:
@IBDesignable class CustomTextView: UIView {
private(set) var textView: UITextView = UITextView(frame: CGRect.zero)
private(set) var placeholderLabel: UILabel = UILabel(frame: CGRect.zero)
@IBInspectable var placeholder: String? {
didSet {
placeholderLabel.text = placeholder
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
private func setup() {
textView.delegate = self
addSubview(textView)
textView.backgroundColor = UIColor.clear
textView.textAlignment = .center
addSubview(placeholderLabel)
placeholderLabel.textAlignment = .center
placeholderLabel.numberOfLines = 0
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
}
override func layoutSubviews() {
super.layoutSubviews()
refreshViews()
}
fileprivate func refreshViews() {
UIView.performWithoutAnimation {
textView.frame = self.bounds
let optimalSize = textView.sizeThatFits(bounds.size)
textView.frame = CGRect(x: 0.0, y: max((bounds.size.height-optimalSize.height)*0.5, 0.0), width: bounds.width, height: min(optimalSize.height, bounds.size.height))
placeholderLabel.frame = bounds
placeholderLabel.backgroundColor = UIColor.clear
if textView.text.isEmpty && textView.isFirstResponder == false {
placeholderLabel.text = placeholder
placeholderLabel.isHidden = false
textView.isHidden = true
} else {
placeholderLabel.isHidden = true
textView.isHidden = false
}
}
}
@objc private func onTap() {
if !textView.isFirstResponder {
textView.becomeFirstResponder()
}
}
}
extension CustomTextView: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
refreshViews()
}
func textViewDidBeginEditing(_ textView: UITextView) {
refreshViews()
}
func textViewDidEndEditing(_ textView: UITextView) {
refreshViews()
}
}
There are still a few downsides though:
- The delegate for the text view is needed by the view itself. So if you would call
myView.textView.delegate = self
in some view controller you would break the logic inside. If you need it I suggest you create a custom delegate on this view and expose the methods you need instead of the standard UITextViewDelegate
.
- Although the class is designable you can not have many inspectable fields. So it is a bit hard to set any properties inside the interface builder. You could expose properties such as "text" and put setter and getter to point to the text view. Could do the same for text color... But the biggest problem is you cant mark fonts as inspectable (cry to Apple I guess).
- In some cases text jumps a bit when going into newline. I guess you would need to play with it a bit to perfect it. It might be that constraints would fix this so if you make it in xib this could solve it.
As a good thing though you can do pretty much anything from the code. Multiline or attributed string placeholders, alignments, colors, fonts...