skip to Main Content

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?

  • I replaced this NSMakeRange line with following (other suggestion I read but same result, doesn’t work) -> textView.selectedTextRange = textView.textRange(from: textView.beginningOfDocument, to: textView.beginningOfDocument)

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()
    }

2

Answers


  1. 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.

    Login or Signup to reply.
  2. 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:

    1. create a label and embed it into the text view at the proper spot.
    2. monitor the tintColor of the text view and update the textColor of the label.
    3. monitor the font of the text view and update the font of the label.
    4. monitor the text of the text view and update the isHidden property of the label based on whether there is any text.
    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)
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search