Индекс символов в точке касания для UILabel

Для UILabel я хотел бы узнать, какой индекс символа находится в определенной точке, полученной от события касания. Я хотел бы решить эту проблему для iOS 7 с помощью Text Kit.

Поскольку UILabel не предоставляет доступ к своему NSLayoutManager , я создал свой собственный на UILabel конфигурации UILabel следующим образом:

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

Код выше находится в подclassе UILabel с UITapGestureRecognizer настроенным на вызов textTapped: ( Gist ).

Получившийся индекс символа имеет смысл (увеличивается при нажатии слева направо), но не является правильным (последний символ достигнут примерно в половине ширины метки). Похоже, что размер шрифта или размер текстового контейнера не настроен должным образом, но не может найти проблему.

Я бы очень хотел, чтобы мой class был подclassом UILabel вместо использования UITextView . Кто-нибудь решил эту проблему для UILabel ?

Обновление: я потратил билет DTS на этот вопрос, и инженер Apple рекомендовал переопределить drawTextInRect: UILabel drawTextInRect: с реализацией, которая использует мой собственный менеджер макетов, подобный этому fragmentу кода:

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

Я думаю, что было бы очень UITextView сохранить мой собственный менеджер макетов синхронно с настройками ярлыка, поэтому я, вероятно, поеду с UITextView несмотря на мое предпочтение UILabel .

Обновление 2: я решил использовать UITextView конце концов. objective всего этого заключалась в обнаружении ответвлений ссылок, встроенных в текст. Я пытался использовать NSLinkAttributeName , но эта настройка не NSLinkAttributeName обратный вызов делегата при быстром нажатии ссылки. Вместо этого вам нужно нажать ссылку на определенное время – очень раздражает. Поэтому я создал CCHLinkTextView , у которого нет этой проблемы.

Я играл с решением Алексея Ишкова. Наконец я получил решение! Используйте этот fragment кода в селекторе 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]; 

Надеюсь, это поможет некоторым людям!

я получил ту же ошибку, что и вы, индекс увеличил скорость, чтобы ускорить, поэтому в конце она не была точной. Причиной этой проблемы было то, что self.attributedText не содержал полную информацию о шрифтах для всей строки.

Когда UILabel отображает, он использует шрифт, указанный в self.font и применяет его ко всему атрибутированномуString. Это не тот случай, когда атрибут присваивается textStorage. Поэтому вам нужно сделать это самостоятельно:

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

Надеюсь это поможет 🙂

Здесь вы – моя реализация для той же проблемы. Мне нужно было отметить #hashtags и #hashtags реакцию на краны.

Я не переопределяю drawTextInRect:(CGRect)rect потому что метод по умолчанию работает отлично.

Также я нашел следующую хорошую реализацию https://github.com/Krelborn/KILabel . Я также использовал некоторые идеи из этого образца.

 @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, синтезированный из многих источников, включая хорошие ответы здесь. Мой вклад – правильная обработка вставки, выравнивания и многострочных меток. (большинство реализаций обрабатывают нажатие на завершающем пробеле в качестве нажатия на конечный символ в строке)

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

Я реализовал то же самое на swift 3. Ниже приведен полный код, чтобы найти индекс символов в точке касания для UILabel, он может помочь другим, которые работают над быстрым и ищут решение:

  //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) 
Interesting Posts
Давайте будем гением компьютера.