1

What I'm trying to do:

There is an attributed text in UITextView, when user tap somewhere on text (not selecting text, just placing blinking cursor somewhere else) the whole sentence around cursor changes color.

How I tried to do this:

   private func setFocus(boundarySymbols: CharacterSet) {
    let mutableString = NSMutableAttributedString(attributedString: noteTextView.attributedText)

    mutableString.beginEditing()

    unfocusWholeText(mutableString: mutableString)
    let selectedRange = noteTextView.selectedRange
    guard let caretPosition = findCaretPosition() else { return }
    guard let focusRange = findRangeToFocus(in: noteTextView.text, around: caretPosition, of: boundarySymbols) else { return }
    text.addAttribute(NSForegroundColorAttributeName, value: ThemeManager.theme.note.text, range: focusRange)

    mutableString.endEditing()

    noteTextView.attributedText = mutableString
    noteTextView.selectedRange = selectedRange
}

Works great when I'm calling this function in shouldChangeTextIn method of UITextViewDelegate, but it works only when user is typing, I would like to call this also when user only changes selection so textViewDidChangeSelection seems to fit perfectly.

But there is a problem:

When I assign modified attributedString to UITextView it moves cursor to the end of text (fixed it with saving cursor position and setting it again after all changes). But the problem is that both of these things are moving cursor and that invokes textViewDidChangeSelection again so infinite loop happens (textViewDidChangeSelection is calling my setFocus which is modifing cursor position so is calling textViewDidChangeSelection again).

So the question is:

Can I change attributes (basically just color) of attributedString without creating NSMutableAttributedString and reassigning it to UITextView.attributedText again to omit moving cursor to end of the text and back?

Or maybe there is a way to somehow turn off moving that cursor around when it shouldn't or make it not calling delegate for a while?

szooky
  • 177
  • 1
  • 12

2 Answers2

1

After a long battle I ended up with simple workaround which didn't came to my mind earlier.

In setFocus placed in textViewDidChangeSelection function I set UITextViewDelegate to nil and after changing colors and setting cursor I assign delegate again. Not a clean solution, but works at least.

szooky
  • 177
  • 1
  • 12
0

I wrote a UITextField extension for one of my projects that lets you do this:

extension UITextField {

    enum ShouldChangeCursor {
        case incrementCursor
        case preserveCursor
    }

    func preserveCursorPosition(withChanges mutatingFunction: (UITextPosition?) -> (ShouldChangeCursor)) {

        //save the cursor positon
        var cursorPosition: UITextPosition? = nil
        if let selectedRange = self.selectedTextRange {
            let offset = self.offset(from: self.beginningOfDocument, to: selectedRange.start)
            cursorPosition = self.position(from: self.beginningOfDocument, offset: offset)
        }

        //make mutaing changes that may reset the cursor position
        let shouldChangeCursor = mutatingFunction(cursorPosition)

        //restore the cursor
        if var cursorPosition = cursorPosition {

            if shouldChangeCursor == .incrementCursor {
                cursorPosition = self.position(from: cursorPosition, offset: 1) ?? cursorPosition
            }

            if let range = self.textRange(from: cursorPosition, to: cursorPosition) {
                self.selectedTextRange = range
            }
        }

    }

}

You can use it to update the attributed text without changing the cursor position:

textField.preserveCursorPosition(withChanges: { _ in
    textField.attributedText = ...
    return .preserveCursor
})
Cal Stephens
  • 725
  • 9
  • 20
  • You are saving cursor position and setting it again later the same as me, it will call `textViewDidChangeSelection` delegate anyway so it won't help in my case – szooky Aug 18 '17 at 12:18