Character index at touch point for UILabel Character index at touch point for UILabel objective-c objective-c

Character index at touch point for UILabel


I played around with the solution of Alexey Ishkov. Finally i got a solution!Use this code snippet in your UITapGestureRecognizer selector:

UILabel *textLabel = (UILabel *)recognizer.view;CGPoint tapLocation = [recognizer locationInView:textLabel];// init text storageNSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:textLabel.attributedText];NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];[textStorage addLayoutManager:layoutManager];// init text containerNSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(textLabel.frame.size.width, textLabel.frame.size.height+100) ];textContainer.lineFragmentPadding  = 0;textContainer.maximumNumberOfLines = textLabel.numberOfLines;textContainer.lineBreakMode        = textLabel.lineBreakMode;[layoutManager addTextContainer:textContainer];NSUInteger characterIndex = [layoutManager characterIndexForPoint:tapLocation                                inTextContainer:textContainer                                fractionOfDistanceBetweenInsertionPoints:NULL];

Hope this will help some people out there!


i got the same error as you, the index increased way to fast so it wasn't accurate at the end. The cause of this issue was that self.attributedTextdid not contain full font information for the whole string.

When UILabel renders it uses the font specified in self.font and applies it to the whole attributedString. This is not the case when assigning the attributedText to the textStorage. Therefore you need to do this yourself:

NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];[attributedText addAttributes:@{NSFontAttributeName: self.font} range:NSMakeRange(0, self.attributedText.string.length];

Swift 4

let attributedText = NSMutableAttributedString(attributedString: self.attributedText!)attributedText.addAttributes([.font: self.font], range: NSMakeRange(0, attributedText.string.count))

Hope this helps :)


Swift 4, synthesized from many sources including good answers here. My contribution is correct handling of inset, alignment, and multi-line labels. (most implementations treat a tap on trailing whitespace as a tap on the final character in the line)

class TappableLabel: UILabel {    var onCharacterTapped: ((_ label: UILabel, _ characterIndex: Int) -> Void)?    func makeTappable() {        let tapGesture = UITapGestureRecognizer()        tapGesture.addTarget(self, action: #selector(labelTapped))        tapGesture.isEnabled = true        self.addGestureRecognizer(tapGesture)        self.isUserInteractionEnabled = true    }    @objc func labelTapped(gesture: UITapGestureRecognizer) {        // only detect taps in attributed text        guard let attributedText = attributedText, gesture.state == .ended else {            return        }        // Configure NSTextContainer        let textContainer = NSTextContainer(size: bounds.size)        textContainer.lineFragmentPadding = 0.0        textContainer.lineBreakMode = lineBreakMode        textContainer.maximumNumberOfLines = numberOfLines        // Configure NSLayoutManager and add the text container        let layoutManager = NSLayoutManager()        layoutManager.addTextContainer(textContainer)        // Configure NSTextStorage and apply the layout manager        let textStorage = NSTextStorage(attributedString: attributedText)        textStorage.addAttribute(NSAttributedStringKey.font, value: font, range: NSMakeRange(0, attributedText.length))        textStorage.addLayoutManager(layoutManager)        // get the tapped character location        let locationOfTouchInLabel = gesture.location(in: gesture.view)        // account for text alignment and insets        let textBoundingBox = layoutManager.usedRect(for: textContainer)        var alignmentOffset: CGFloat!        switch textAlignment {        case .left, .natural, .justified:            alignmentOffset = 0.0        case .center:            alignmentOffset = 0.5        case .right:            alignmentOffset = 1.0        }        let xOffset = ((bounds.size.width - textBoundingBox.size.width) * alignmentOffset) - textBoundingBox.origin.x        let yOffset = ((bounds.size.height - textBoundingBox.size.height) * alignmentOffset) - textBoundingBox.origin.y        let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - xOffset, y: locationOfTouchInLabel.y - yOffset)        // figure out which character was tapped        let characterTapped = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)        // figure out how many characters are in the string up to and including the line tapped        let lineTapped = Int(ceil(locationOfTouchInLabel.y / font.lineHeight)) - 1        let rightMostPointInLineTapped = CGPoint(x: bounds.size.width, y: font.lineHeight * CGFloat(lineTapped))        let charsInLineTapped = layoutManager.characterIndex(for: rightMostPointInLineTapped, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)        // ignore taps past the end of the current line        if characterTapped < charsInLineTapped {            onCharacterTapped?(self, characterTapped)        }    }}