Search code examples
iosswiftnsattributedstring

permanently append text to a UITextView


I'm trying a create a UITextView that upon selection adds the users handle to the beginning of the text.

I understand it is possible to create another UITextView and place it to the left of the second UITextView to make it look like they are part of the same range but for formatting purposes it would be much easier if both parts were part of the same textview...

Here's the function I'm using along with an example of it being used.

func addHandle(text:String, handle:String) -> NSAttributedString {

    let convertedText = NSMutableAttributedString()

    let h = NSMutableAttributedString(string: handle)
    h.beginEditing()

    let m = NSMutableAttributedString(string: text)
    m.beginEditing()


    do {
        m.addAttributes([NSFontAttributeName: UIFont.init(name: "PingFangSC-Regular", size: 18)], range: (m.string as NSString).range(of: m.string))
        h.addAttributes([NSFontAttributeName: UIFont.init(name: "PingFangSC-Medium", size: 18)], range: (h.string as NSString).range(of: h.string))

        convertedText.append(h)
        convertedText.append(m)

        m.endEditing()
        h.endEditing()
    }
    return convertedText
}

let myHandle = "Johnny: "
let myMessage = "This is a test"

addHandle(text: myMessage, handle: myHandle)

In this scenario the both handle and message are able to be concatenated.

However when the text is changed, as seen below, since the handle is part of the new text it runs the function again and adds the handle again.

func textViewDidBeginEditing(_ textView: UITextView) {
    let currentString: String = textView.text!
    self.t.searchBar.attributedText = addHandle(text: currentString, handle: "Johnny: ")
}

 func textViewDidChange(_ textView: UITextView) {

    let currentString: String = textView.text!
    self.t.searchBar.attributedText = addHandle(text: currentString, handle: "Johnny: ")

    UIView.animate(withDuration: 0.15, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {
        self.view.layoutIfNeeded()
    }, completion: { (completed) in

    })
}

How should I go about modifying the my function or implementation to ensure that

  1. The handle is always first before the message
  2. The user cannot erase the handle from the text

The following should be possible:

Johhny: Test Message

Johnny:

This should not be possible

Johnn

Any suggestions?

EDIT: Here's a screenshot displaying the current behavior. The handle is "Johnny: "and the text typed was "This"

enter image description here


Solution

  • I think you should use something like that (note I used textField, but you can made the same for textView):

    public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
       var newString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
       if !newString.hasPrefix(handle) {
           newString = "\(handle): \(newString)"
       }
       textView.text = newString
    
       // Optional font color change for the handle
       let attributedString = NSMutableAttributedString(string: textView.text!, attributes: [ NSFontAttributeName : generalFont, NSForegroundColorAttributeName: textView.textColor ]);
       attributedString.addAttribute(NSFontAttributeName, value: anotherFont, range: NSMakeRange(0, handle.characters.count));
       textView.attributedText = attributedString
       return false
    }
    

    Also you can try to check range and deny editing (return false) if the range covers the first characters of the string (where you handle is placed).