Allison
Allison

Reputation: 2383

CTRunGetImageBounds returns incorrect x coordinate on iOS 10 and below

I'm working with CoreText and am trying to add link support to a custom label on iOS 11/12 however I need to support older software versions too. To do this, I check if the user's touch intersects and of the link text bounds.

To get the bounds, I simply call CTRunGetImageBounds(run, context, CFRangeMake(0, 0)). This works perfectly on iOS 11 and 12 but is broken on 9 and 10. I get a rect back along the lines of (250,8.1,100,30) on iOS 11 but on iOS 9, however, the same function call returns (0.1,8.1,100,30). The x coordinate seems to be relative to the run and not the actual frame. Since the coordinates are invalid, links cannot be correctly clicked on iOS 9/10 which is a problem for obvious reasons.

I've included a picture comparing the two. The expected behavior is the links should be highlighted with the green rectangles. Note that the iOS 12 simulator does this correctly while 9.3 has all the rectangles slammed to the left due to the near zero x coordinate.

iOS 12 simulator iOS 9.3 simulator

Upvotes: 1

Views: 139

Answers (1)

Allison
Allison

Reputation: 2383

Fixed iOS 9.3

This seems to either be a CoreText bug or some behavior Apple changed without documenting. On iOS 9 CTRunGetImageBounds returns the an x value relative to itself, not the greater frame. This means while the height and width are correct, the x coordinate returned is simply useless. Since this is on a long dead firmware, it's not going to get fixed but I found a work around anyways!

Swift 4

/// Get the CoreText relative frame for a given CTRun. This method works around an iOS <=10 CoreText bug in CTRunGetImageBounds
///
/// - Parameters:
///   - run: The run
///   - context: Context, used by CTRunGetImageBounds
/// - Returns: A tight fitting, CT rect that fits around the run
func getCTRectFor(run:CTRun,context:CGContext) -> CGRect {
    let imageBounds = CTRunGetImageBounds(run, context, CFRangeMake(0, 0))
    if #available(iOS 11.0, *) {
        //Non-bugged iOS, can assume the bounds are correct
        return imageBounds
    } else {
        //<=iOS 10 has a bug with getting the frame of a run where it gives invalid x positions
        //The CTRunGetPositionsPtr however works as expected and returns the correct position. We can take that value and substitute it
        let runPositionsPointer = CTRunGetPositionsPtr(run)
        if let runPosition = runPositionsPointer?.pointee {
            return CGRect.init(x: runPosition.x, y: imageBounds.origin.y, width: imageBounds.width, height: imageBounds.height)
        }else {
            //FAILED TO OBTAIN RUN ORIGIN? FALL BACK.
            return imageBounds
        }
    }
}

While CTRunGetImageBounds returns an invalid x coordinate, we can use CTRunGetPositionsPtr to fix it because it returns the correct origin for the run. To get the full, correct frame for a range we simply need to replace the x value from CTRunGetImageBounds with our new value.

This will likely be a bit slower however only by a fraction of a millisecond.

Upvotes: 1

Related Questions