Reputation: 2383
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.
Upvotes: 1
Views: 139
Reputation: 2383
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!
/// 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