Reputation: 107
I spent a lot of time trying to fix this simple issues but don't managed it. I want in UITextView to use a placeholder: "translation" and when tapping the UITextView the placeholder should stay there until a character is entered but I need the cursor to be placed at the beginning of the placeholder just before "translation" word but it stays at the end of my placeholder. I find several answers and tried to do it as following but doesn't work. The placeholder changes correctly to black colour but the 2nd command -> textView.selectedRange = NSMakeRange(0, 0) is not moving the cursor to the beginning, why?
Here the full code:
class MainViewController: UIViewController, UITextViewDelegate, UITextFieldDelegate {
@IBOutlet weak var definitionField: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// setting placeholder
definitionField.textColor = .gray
definitionField.text = "translation"
definitionField.delegate = self }
func textViewDidBeginEditing(_ textView: UITextView) {
if (textView.text == "translation") {
textView.textColor = .black
textView.selectedRange = NSMakeRange(0, 0)
}
}
func textViewDidEndEditing(_ textView: UITextView) {
if (textView.text == "") {
textView.text = "translation"
textView.textColor = .gray
}
textView.resignFirstResponder()
}
Upvotes: 1
Views: 409
Reputation: 33979
The simplest solution here is to add a label that holds the placeholder. Then set its isHidden
property based on whether there is any text in the text view.
Here is a working example but it uses RxSwift to track text, color, and font changes.
What it does:
extension UITextView {
func withPlaceholder(_ placeholder: String) {
let label = {
let result = UILabel(frame: bounds)
result.text = placeholder
result.numberOfLines = 0
result.frame = result.frame.offsetBy(dx: 4, dy: 8)
return result
}()
addSubview(label)
_ = rx.observe(UIColor.self, "tintColor")
.take(until: rx.deallocating)
.bind(to: label.rx.textColor)
_ = rx.observe(UIFont.self, "font")
.map { $0 != nil ? $0 : UIFont.systemFont(ofSize: 12) }
.take(until: rx.deallocating)
.bind(onNext: { [weak self] font in
label.font = font
label.frame.size = label.sizeThatFits(self?.bounds.size ?? CGSize.zero)
})
_ = rx.text.orEmpty.map { !$0.isEmpty }
.take(until: rx.deallocating)
.bind(to: label.rx.isHidden)
}
}
Upvotes: 0
Reputation: 274480
It appears that selectedTextRange
is being set automatically by the system, after textViewDidBeginEditing
, and becomeFirstResponder
is called. You can observe this by using a custom UITextView
subclass, and overriding:
override var selectedTextRange: UITextRange? {
didSet {
print(selectedTextRange)
}
}
You will see that this first gets set to (0, 0)
because of your code, and then didSet
is "magically" called again, setting selectedTextRange
to (11, 0)
.
I'm not sure which method did the call. There might not even be a overridable method that gets called after that.
A simple solution would be to just use DispatchQueue.main.async
to wait for whatever is setting the selected text range to the end, and then change the selection.
// in textViewDidBeginEditing
DispatchQueue.main.async {
textView.selectedRange = NSMakeRange(0, 0)
}
The cursor will appear at the end for a brief moment, before going to the start. This seems to be only visual. When I tried to enter text when the cursor is at the end, the text is inserted before "translation".
That said, your implementation of this placeholder seems to have rather different from how one would expect a placeholder to work (like the one in UITextField
), especially when textViewDidEndEditing
.
If you want behaviours similar to UITextField
, or if you want to see other approaches, check out this post.
Upvotes: 1