Indice dei caratteri al punto di contatto per UILabel

Per un UILabel , mi piacerebbe scoprire quale indice di carattere è in un punto specifico ricevuto da un evento touch. Mi piacerebbe risolvere questo problema per iOS 7 usando Text Kit.

Dato che UILabel non fornisce l’accesso a NSLayoutManager , ho creato il mio basato sulla configurazione di UILabel questo modo:

 - (void)textTapped:(UITapGestureRecognizer *)recognizer { if (recognizer.state == UIGestureRecognizerStateEnded) { CGPoint location = [recognizer locationInView:self]; NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText]; NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; [textStorage addLayoutManager:layoutManager]; NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.bounds.size]; [layoutManager addTextContainer:textContainer]; textContainer.maximumNumberOfLines = self.numberOfLines; textContainer.lineBreakMode = self.lineBreakMode; NSUInteger characterIndex = [layoutManager characterIndexForPoint:location inTextContainer:textContainer fractionOfDistanceBetweenInsertionPoints:NULL]; if (characterIndex < textStorage.length) { NSRange range = NSMakeRange(characterIndex, 1); NSString *value = [self.text substringWithRange:range]; NSLog(@"%@, %zd, %zd", value, range.location, range.length); } } } 

Il codice sopra è in una sottoclass UILabel con un UITapGestureRecognizer configurato per chiamare textTapped: ( Gist ).

L’indice dei caratteri risultante ha senso (aumenta quando si tocca da sinistra a destra), ma non è corretto (l’ultimo carattere è raggiunto a circa metà della larghezza dell’etichetta). Sembra che la dimensione del carattere o la dimensione del contenitore di testo non siano configurate correttamente, ma non riescano a trovare il problema.

Mi piacerebbe davvero mantenere la mia class una sottoclass di UILabel invece di usare UITextView . Qualcuno ha risolto questo problema per UILabel ?

    Aggiornamento: ho speso un ticket DTS su questa domanda e l’ingegnere Apple ha consigliato di sovrascrivere UILabel di drawTextInRect: con un’implementazione che utilizza il mio gestore di layout, simile a questo frammento di codice:

     - (void)drawTextInRect:(CGRect)rect { [yourLayoutManager drawGlyphsForGlyphRange:NSMakeRange(0, yourTextStorage.length) atPoint:CGPointMake(0, 0)]; } 

    Penso che sarebbe molto lavoro mantenere il mio gestore di layout in sincronia con le impostazioni dell’etichetta, quindi probabilmente andrò con UITextView nonostante la mia preferenza per UILabel .

    Aggiornamento 2: ho deciso di utilizzare UITextView dopo tutto. Lo scopo di tutto questo era rilevare i tocchi sui collegamenti incorporati nel testo. Ho provato a utilizzare NSLinkAttributeName , ma questa impostazione non ha triggersto la richiamata delegato quando si NSLinkAttributeName rapidamente un collegamento. Invece, devi premere il link per un certo periodo di tempo – molto fastidioso. Così ho creato CCHLinkTextView che non ha questo problema.

    Ho giocato con la soluzione di Alexey Ishkov. Finalmente ho trovato una soluzione! Utilizza questo snippet di codice nel tuo selettore UITapGestureRecognizer:

     UILabel *textLabel = (UILabel *)recognizer.view; CGPoint tapLocation = [recognizer locationInView:textLabel]; // init text storage NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:textLabel.attributedText]; NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; [textStorage addLayoutManager:layoutManager]; // init text container NSTextContainer *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]; 

    Spero che questo aiuti alcune persone là fuori!

    ho avuto lo stesso errore di te, l’indice è aumentato rapidamente, quindi alla fine non è stato preciso. La causa di questo problema era che self.attributedText non conteneva informazioni complete sui font per l’intera stringa.

    Quando UILabel esegue il rendering usa il font specificato in self.font e lo applica all’intero attributo. Questo non è il caso quando si assegna il testo attribuito a textStorage. Pertanto è necessario farlo da soli:

     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)) 

    Spero che questo ti aiuti 🙂

    Ecco la mia implementazione per lo stesso problema. Ho dovuto contrassegnare #hashtags e @usernames con la reazione sui rubinetti.

    Non sovrascrivo drawTextInRect:(CGRect)rect perché il metodo predefinito funziona perfettamente.

    Inoltre ho trovato la seguente bella implementazione https://github.com/Krelborn/KILabel . Ho usato alcune idee anche da questo esempio.

     @protocol EmbeddedLabelDelegate  - (void)embeddedLabelDidGetTap:(EmbeddedLabel *)embeddedLabel; - (void)embeddedLabel:(EmbeddedLabel *)embeddedLabel didGetTapOnHashText:(NSString *)hashStr; - (void)embeddedLabel:(EmbeddedLabel *)embeddedLabel didGetTapOnUserText:(NSString *)userNameStr; @end @interface EmbeddedLabel : UILabel @property (nonatomic, weak) id delegate; - (void)setText:(NSString *)text; @end #define kEmbeddedLabelHashtagStyle @"hashtagStyle" #define kEmbeddedLabelUsernameStyle @"usernameStyle" typedef enum { kEmbeddedLabelStateNormal = 0, kEmbeddedLabelStateHashtag, kEmbeddedLabelStateUsename } EmbeddedLabelState; @interface EmbeddedLabel () @property (nonatomic, strong) NSLayoutManager *layoutManager; @property (nonatomic, strong) NSTextStorage *textStorage; @property (nonatomic, weak) NSTextContainer *textContainer; @end @implementation EmbeddedLabel - (void)dealloc { _delegate = nil; } - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self setupTextSystem]; } return self; } - (void)awakeFromNib { [super awakeFromNib]; [self setupTextSystem]; } - (void)setupTextSystem { self.userInteractionEnabled = YES; self.numberOfLines = 0; self.lineBreakMode = NSLineBreakByWordWrapping; self.layoutManager = [NSLayoutManager new]; NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.bounds.size]; textContainer.lineFragmentPadding = 0; textContainer.maximumNumberOfLines = self.numberOfLines; textContainer.lineBreakMode = self.lineBreakMode; textContainer.layoutManager = self.layoutManager; [self.layoutManager addTextContainer:textContainer]; self.textStorage = [NSTextStorage new]; [self.textStorage addLayoutManager:self.layoutManager]; } - (void)setFrame:(CGRect)frame { [super setFrame:frame]; self.textContainer.size = self.bounds.size; } - (void)setBounds:(CGRect)bounds { [super setBounds:bounds]; self.textContainer.size = self.bounds.size; } - (void)layoutSubviews { [super layoutSubviews]; self.textContainer.size = self.bounds.size; } - (void)setText:(NSString *)text { [super setText:nil]; self.attributedText = [self attributedTextWithText:text]; self.textStorage.attributedString = self.attributedText; [self.gestureRecognizers enumerateObjectsUsingBlock:^(UIGestureRecognizer *recognizer, NSUInteger idx, BOOL *stop) { if ([recognizer isKindOfClass:[UITapGestureRecognizer class]]) [self removeGestureRecognizer:recognizer]; }]; [self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(embeddedTextClicked:)]]; } - (NSMutableAttributedString *)attributedTextWithText:(NSString *)text { NSMutableParagraphStyle *style = [NSMutableParagraphStyle new]; style.alignment = self.textAlignment; style.lineBreakMode = self.lineBreakMode; NSDictionary *hashStyle = @{ NSFontAttributeName : [UIFont boldSystemFontOfSize:[self.font pointSize]], NSForegroundColorAttributeName : (self.highlightedTextColor ?: (self.textColor ?: [UIColor darkTextColor])), NSParagraphStyleAttributeName : style, kEmbeddedLabelHashtagStyle : @(YES) }; NSDictionary *nameStyle = @{ NSFontAttributeName : [UIFont boldSystemFontOfSize:[self.font pointSize]], NSForegroundColorAttributeName : (self.highlightedTextColor ?: (self.textColor ?: [UIColor darkTextColor])), NSParagraphStyleAttributeName : style, kEmbeddedLabelUsernameStyle : @(YES) }; NSDictionary *normalStyle = @{ NSFontAttributeName : self.font, NSForegroundColorAttributeName : (self.textColor ?: [UIColor darkTextColor]), NSParagraphStyleAttributeName : style }; NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:@"" attributes:normalStyle]; NSCharacterSet *charSet = [NSCharacterSet characterSetWithCharactersInString:kWhiteSpaceCharacterSet]; NSMutableString *token = [NSMutableString string]; NSInteger length = text.length; EmbeddedLabelState state = kEmbeddedLabelStateNormal; for (NSInteger index = 0; index < length; index++) { unichar sign = [text characterAtIndex:index]; if ([charSet characterIsMember:sign] && state) { [attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:token attributes:state == kEmbeddedLabelStateHashtag ? hashStyle : nameStyle]]; state = kEmbeddedLabelStateNormal; [token setString:[NSString stringWithCharacters:&sign length:1]]; } else if (sign == '#' || sign == '@') { [attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:token attributes:normalStyle]]; state = sign == '#' ? kEmbeddedLabelStateHashtag : kEmbeddedLabelStateUsename; [token setString:[NSString stringWithCharacters:&sign length:1]]; } else { [token appendString:[NSString stringWithCharacters:&sign length:1]]; } } [attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:token attributes:state ? (state == kEmbeddedLabelStateHashtag ? hashStyle : nameStyle) : normalStyle]]; return attributedText; } - (void)embeddedTextClicked:(UIGestureRecognizer *)recognizer { if (recognizer.state == UIGestureRecognizerStateEnded) { CGPoint location = [recognizer locationInView:self]; NSUInteger characterIndex = [self.layoutManager characterIndexForPoint:location inTextContainer:self.textContainer fractionOfDistanceBetweenInsertionPoints:NULL]; if (characterIndex < self.textStorage.length) { NSRange range; NSDictionary *attributes = [self.textStorage attributesAtIndex:characterIndex effectiveRange:&range]; if ([attributes objectForKey:kEmbeddedLabelHashtagStyle]) { NSString *value = [self.attributedText.string substringWithRange:range]; [self.delegate embeddedLabel:self didGetTapOnHashText:[value stringByReplacingOccurrencesOfString:@"#" withString:@""]]; } else if ([attributes objectForKey:kEmbeddedLabelUsernameStyle]) { NSString *value = [self.attributedText.string substringWithRange:range]; [self.delegate embeddedLabel:self didGetTapOnUserText:[value stringByReplacingOccurrencesOfString:@"@" withString:@""]]; } else { [self.delegate embeddedLabelDidGetTap:self]; } } else { [self.delegate embeddedLabelDidGetTap:self]; } } } @end 

    Swift 4, sintetizzato da molte fonti tra cui buone risposte qui. Il mio contributo è la corretta gestione di inset, allineamento e etichette multilinea. (la maggior parte delle implementazioni trattano un touch sugli spazi bianchi finali come un touch sul carattere finale nella linea)

     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) } } } 

    Ho implementato lo stesso su swift 3. Di seguito è riportato il codice completo per trovare l’indice di carattere al punto di contatto per UILabel, può aiutare gli altri che stanno lavorando su swift e cercando la soluzione:

      //here myLabel is the object of UILabel //added this from @warly's answer //set font of attributedText let attributedText = NSMutableAttributedString(attributedString: myLabel!.attributedText!) attributedText.addAttributes([NSFontAttributeName: myLabel!.font], range: NSMakeRange(0, (myLabel!.attributedText?.string.characters.count)!)) // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage let layoutManager = NSLayoutManager() let textContainer = NSTextContainer(size: CGSize(width: (myLabel?.frame.width)!, height: (myLabel?.frame.height)!+100)) let textStorage = NSTextStorage(attributedString: attributedText) // Configure layoutManager and textStorage layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) // Configure textContainer textContainer.lineFragmentPadding = 0.0 textContainer.lineBreakMode = myLabel!.lineBreakMode textContainer.maximumNumberOfLines = myLabel!.numberOfLines let labelSize = myLabel!.bounds.size textContainer.size = labelSize // get the index of character where user tapped let indexOfCharacter = layoutManager.characterIndex(for: tapLocation, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)