UILabel触摸点的字符索引

时间:2014-01-25 11:06:45

标签: objective-c ios7 textkit

对于UILabel,我想知道哪个字符索引位于从触摸事件接收的特定点。我想使用Text Kit为iOS 7解决这个问题。

由于UILabel不提供对其NSLayoutManager的访问权限,因此我根据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);
        }
    }
}

上面的代码位于UILabel子类中,UITapGestureRecognizer配置为调用textTapped:Gist)。

生成的字符索引有意义(从左到右点击时增加),但不正确(最后一个字符大约是标签宽度的一半)。看起来可能没有正确配置字体大小或文本容器大小,但找不到问题。

我真的想让我的班级成为UILabel的子类而不是UITextView。有人为UILabel解决了这个问题吗?

更新:我在这个问题上花了一张DTS票,而Apple工程师建议用一个使用我自己的布局管理器的实现覆盖UILabel的{​​{1}},类似到此代码段:

drawTextInRect:

我认为让自己的布局管理器与标签的设置保持同步会有很多工作,所以尽管我更喜欢- (void)drawTextInRect:(CGRect)rect { [yourLayoutManager drawGlyphsForGlyphRange:NSMakeRange(0, yourTextStorage.length) atPoint:CGPointMake(0, 0)]; } ,我可能会选择UITextView

更新2:毕竟我决定使用UILabel。所有这一切的目的是检测文本中嵌入的链接上的点击。我尝试使用UITextView,但是这个设置在快速点击链接时没有触发委托回调。相反,你必须按下链接一段时间 - 非常烦人。所以我创建了没有这个问题的CCHLinkTextView

7 个答案:

答案 0 :(得分:40)

我玩弄了Alexey Ishkov的解决方案。最后我得到了解决方案! 在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 :(得分:16)

我得到了与你相同的错误,索引增加了快速的方式,所以最后它并不准确。导致此问题的原因是self.attributedText未包含整个字符串的完整字体信息。

当UILabel渲染时,它使用self.font中指定的字体并将其应用于整个referencedString。将attributionText分配给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))

希望这会有所帮助:)

答案 2 :(得分:6)

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

答案 3 :(得分: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

答案 4 :(得分:2)

我在SwiftUI的UIViewRepresentable上下文中使用它,并尝试向其添加链接。我在这些答案中发现的代码均不正确(特别是对于多行代码),这是我所能得到的精确(和简洁)的结果:

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

答案 5 :(得分:1)

我在swift 3上实现了相同的功能。下面是在UILabel的触点处找到Character索引的完整代码,它可以帮助其他正在研究swift并寻找解决方案的人:

// Server exposes all the methods that Logger has
type Server struct {
    Host string
    Port int
    *log.Logger
}

// initialize the embedded type the usual way
server := &Server{"localhost", 80, log.New(...)}

// methods implemented on the embedded struct are passed through
server.Log(...) // calls server.Logger.Log(...)

// the field name of the embedded type is its type name (in this case Logger)
var logger *log.Logger = server.Logger

答案 6 :(得分:0)

雨燕5

 extension UITapGestureRecognizer {

 func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
    // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
    let layoutManager = NSLayoutManager()
    let textContainer = NSTextContainer(size: CGSize.zero)
    let textStorage = NSTextStorage(attributedString: label.attributedText!)

    // Configure layoutManager and textStorage
    layoutManager.addTextContainer(textContainer)
    textStorage.addLayoutManager(layoutManager)

    // Configure textContainer
    textContainer.lineFragmentPadding = 0.0
    textContainer.lineBreakMode = label.lineBreakMode
    textContainer.maximumNumberOfLines = label.numberOfLines
    let labelSize = label.bounds.size
    textContainer.size = labelSize

    // Find the tapped character location and compare it to the specified range
    let locationOfTouchInLabel = self.location(in: label)
    let textBoundingBox = layoutManager.usedRect(for: textContainer)
    let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                                      y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
    let locationOfTouchInTextContainer = CGPoint(x: (locationOfTouchInLabel.x - textContainerOffset.x),
                                                 y: 0 );
    // Adjust for multiple lines of text
    let lineModifier = Int(ceil(locationOfTouchInLabel.y / label.font.lineHeight)) - 1
    let rightMostFirstLinePoint = CGPoint(x: labelSize.width, y: 0)
    let charsPerLine = layoutManager.characterIndex(for: rightMostFirstLinePoint, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)

    let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
    let adjustedRange = indexOfCharacter + (lineModifier * charsPerLine)

    return NSLocationInRange(adjustedRange, targetRange)
   }

}

对我有用。