For a UILabel
, I\'d like to find out which character index is at specific point received from a touch event. I\'d like to solve this problem for iOS 7 using Tex
I'm using this in the context of a UIViewRepresentable in SwiftUI, and trying to add links to it. None of the code I found in these answers was quite right (especially for multi-line), and this is as precise (and as clean) as I could get it:
// set up the text engine
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: .zero)
let textStorage = NSTextStorage(attributedString: attrString)
// copy over properties from the label
// assuming left aligned text, might need further adjustments for other alignments
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
let labelSize = label.bounds.size
textContainer.size = labelSize
// hook up the text engine
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// adjust for the layout manager's geometry (not sure exactly how this works but it's required)
let locationOfTouchInLabel = tap.location(in: label)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let textContainerOffset = CGPoint(
x: labelSize.width/2 - textBoundingBox.midX,
y: labelSize.height/2 - textBoundingBox.midY
)
let locationOfTouchInTextContainer = CGPoint(
x: locationOfTouchInLabel.x - textContainerOffset.x,
y: locationOfTouchInLabel.y - textContainerOffset.y
)
// actually perform the check to get the index, accounting for multiple lines
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
// get the attributes at the index
let attributes = attrString.attributes(at: indexOfCharacter, effectiveRange: nil)
// use `.attachment` instead of `.link` so you can bring your own styling
if let url = attributes[.attachment] as? URL {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}