2014-01-25 3 views
20

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

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

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

Код выше в UILabel подкласса с UITapGestureRecognizer сконфигурированной для вызова textTapped: (Gist).

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

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

Update: я провел билет DTS на этот вопрос и инженер Apple, рекомендуется отменить UILabel «s drawTextInRect: с реализацией, которая использует свой собственный менеджер компоновки, похожий на этот фрагмент кода:

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

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

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

+0

Поздняя реакция; трюк для меня, чтобы заставить это работать, это строка 'textContainer.lineFragmentPadding = 0;', которая отсутствует в вашем примере, но присутствует в ответах ниже на @Alexey Ishkov и @Kai Burghardt. Мне не пришлось взломать контейнер с номером 100. – Koen

ответ

5

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

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

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

@protocol EmbeddedLabelDelegate <NSObject> 
- (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<EmbeddedLabelDelegate> 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 
36

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

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

+1

На самом деле вам нужно слегка настроить testStorage по сравнению с оригинальным размером метки. Это эмпирический факт, что для каждой дополнительной строки UILabel вам нужно добавить около 1pt к высоте. Поэтому textStorage следует устанавливать динамически в зависимости от количества строк. – malex

+7

Можете ли вы объяснить, почему ... textLabel.frame.size.height + 100 magic number? – tiritea

+0

Убедитесь, что вы сначала вызываете sizeToFit на UILabel – user1055568

10

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

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

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

Swift 3

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

Надеется, что это помогает :)

1

Я выполнил то же самое на быстром 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) 
1

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

Я использовал этот код для получения символьного индекса при нажатии на метку. Он отлично работает на первой линии. Но он не возвращает правильный индекс на линии secon, возвращает только последний индекс символов. Есть ли способ решить эту проблему. Я проверил правильную позицию. но в layoutManager возвращается индекс false. – mkjwa

+0

Некоторый я нашел из-за linebreakmode. В случае хвоста Truncate layoutManager рассматривается как одна строка. В случае Word Wrap, он хорошо работает в нескольких линиях. Этот код очень полезен. Благодарю. – mkjwa