2

I'm trying to do syntax highlighting to mark errors on user input.

Following the suggestions in Get X and Y coordinates of a word in UITextView my code works perfectly for UITextView

However, I'm now trying to do a similar thing for a UITextField and having trouble.

Both UITextField and UITextView conform to UITextInput and all the methods required to locate a rect should work for both.

Here's a func for UITextView

func findRect(forTextMatching match:String, in textView:UITextView) -> CGRect? {
    if let text = textView.text,
       let range = text.range(of: match)
    {
        let offset1 = text.distance(from: text.startIndex, to: range.lowerBound)
        let offset2 = text.distance(from: text.startIndex, to: range.upperBound)

        if let pos1 = textView.position(from: textView.beginningOfDocument, offset: offset1),
        let pos2 = textView.position(from: textView.beginningOfDocument, offset: offset2)
        {
            if let textRange = textView.textRange(from: pos1, to: pos2)
            {
                textView.selectedTextRange = textRange
                let rect = textView.firstRect(for: textRange)
                return rect
            }
        }
    }

    return nil
}

Run this in a test app and highlight the return rect looks like this:

enter image description here

Now try to do the same thing with UITextField:

func findRect(forTextMatching match:String, in textField:UITextField) -> CGRect? {
    if let text = textField.text,
       let range = text.range(of: match)
    {
        let offset1 = text.distance(from: text.startIndex, to: range.lowerBound)
        let offset2 = text.distance(from: text.startIndex, to: range.upperBound)

        // beginningOfDocument is nil unless textField is first responder
        textField.becomeFirstResponder()
        defer {
            textField.resignFirstResponder()
        }

        if let pos1 = textField.position(from: textField.beginningOfDocument, offset: offset1),
            let pos2 = textField.position(from: textField.beginningOfDocument, offset: offset2)
        {
            if let textRange = textField.textRange(from: pos1, to: pos2)
            {
                textField.selectedTextRange = textRange
                var rect = textField.firstRect(for: textRange)

                return rect
            }
        }
    }

    return nil
}

The first problem is that UITextField.beginningOfDocument is nil unless the field is first responder. That's easy enough to deal with, hence the extra lines to becomeFirstResonder and resignFirstResponder.

Run this in a test app and highlight the returned rect looks like this:

enter image description here

The returned rect is slightly offset.

Dale
  • 3,193
  • 24
  • 29

1 Answers1

0

It seems as if this is a bug with UITextField. Inspecting the view hierarchy in debug there is an extra view under UITextField of type UIFieldEditor with a frame origin (7,4). The returned rect seems to be relative to this view, not UITextField.

It turns out you can access this frame as the editRect of of UITextField.

Modified func for UITextField:

func findRect(forTextMatching match:String, in textField:UITextField) -> CGRect? {
    if let text = textField.text,
       let range = text.range(of: match)
    {
        let offset1 = text.distance(from: text.startIndex, to: range.lowerBound)
        let offset2 = text.distance(from: text.startIndex, to: range.upperBound)

        // beginningOfDocument is nil unless textField is first responder
        textField.becomeFirstResponder()
        defer {
            textField.resignFirstResponder()
        }

        if let pos1 = textField.position(from: textField.beginningOfDocument, offset: offset1),
            let pos2 = textField.position(from: textField.beginningOfDocument, offset: offset2)
        {
            if let textRange = textField.textRange(from: pos1, to: pos2)
            {
                var rect = textField.firstRect(for: textRange)

                // firstRect is actually relative to editingRect, not textField
                let top = textField.editingRect(forBounds: textField.bounds).origin.y
                let left = textField.editingRect(forBounds: textField.bounds).origin.x
                rect = CGRect(x: rect.origin.x+left, y: rect.origin.y+top, width: rect.width, height: rect.height)

                return rect
            }
        }
    }

    return nil
}

Produces correct result:

enter image description here

Dale
  • 3,193
  • 24
  • 29