2
votes

I'm implementing a custom UILabel with link clicking support. To do so, I've got the following methods: one to initialize the structures (called only once) and one to detect clicks in a determinate range of text within a string:

- (void) sharedInit {
    // Remember, this method is only called once
    self.layoutManager = [[NSLayoutManager alloc] init];
    self.textContainer = [[NSTextContainer alloc] initWithSize:self.frame.size];
    [self.layoutManager addTextContainer:self.textContainer];
    self.myGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];

    self.textContainer.lineFragmentPadding = 0.0;
    self.textContainer.lineBreakMode = self.lineBreakMode;
    self.textContainer.maximumNumberOfLines = self.numberOfLines;
    self.textContainer.size = self.frame.size;
}

- (void) onTap:(UITapGestureRecognizer*) tapGesture {
    CGPoint location = [tapGesture locationInView:tapGesture.view];
    NSInteger index = [self.layoutManager characterIndexForPoint:locationOfTouchInLabel
                                                 inTextContainer:self.textContainer
                        fractionOfDistanceBetweenInsertionPoints:nil];

    NSLog(@"index of tapped character: %li", index);

    // ... and some more code to work with that index
}

- (void) setFrame:(CGRect)frame {
    [super setFrame:frame];
    self.textContainer.size = self.frame.size;
}

- (void) setAttributedText:(NSAttributedString *)attributedText {
    [super setAttributedText:attributedText];
    self.textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedText];
    [self.textStorage addLayoutManager:self.layoutManager];
}

- (void) setNumberOfLines:(NSInteger)numberOfLines {
    [super setNumberOfLines:numberOfLines];
    self.textContainer.maximumNumberOfLines = numberOfLines;
}

The key variable here is index (onTap: mehtod). That variable returns the index of the tapped character so we can work with it. This worked flawlessly with iOS 8, but as for iOS 9 I'm seeing the following behavior:

  • If the label has one line, it returns the correct index
  • If the label has more than one line, it always returns zero no matter where did you click.

I faced a similar issue when I was developing the feature for iOS 8, and I solved it by setting the maximumNumberOfLines of the NSTextContainer to the correct value, as you can see in the initialization. The configuration parameters (size, maxNumOfLines, etc) for the TextContainer are correct when the onTap method is run. I've checked the documentation (https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/NSTextContainer_Class_TextKit/index.html#//apple_ref/occ/instm/NSTextContainer/lineFragmentRectForProposedRect:atIndex:writingDirection:remainingRect:) and apparently nothing's changed in this version, so I'm quite lost. So far, this looks like a iOS9 bug, but I don't want to discard any options. I've come with a couple of workarounds as well, but if it's possible I'd like to know what's going on with that method.

So, does anybody know what's going on? Thanks in advance...

EDIT:

Two things:

  • I tried consulting glyphIndex instead of charIndex and so far it's returning identical results (ok with one line, zero with more than one line)
  • I've noticed that the firstUnlaidCharIndex is equal to 0 when the result is incorrect. In cases where it works fine (iOS8 and iOS9 with just one line) it's returning the correct value, the first out-of-bounds character.
2
In my case, the characterIndexForPoint: method consistently returns the index of the last character of the string if I'm anywhere on the last line of a multiline label. I see the same results regardless of whether I create the storage, container and manager dynamically or hold onto them.tyler
@tyler, have you set the numberOfLines attribute of your NSTextContainer? I happened to run into this issue as well, and it was due to that value being equal to 1 (single line-container) even when the label had a 0. That way, clicks beyond the first line were rendered as out of bounds, so equal to the last characterBartserk

2 Answers

7
votes

Try initing the size of the NSTextContainer with the same width of the UILabel and CGFLOAT_MAX height

    self.textContainer.size = CGSizeMake(self.bounds.size.width, CGFLOAT_MAX);
0
votes

Solved! Looks like the problem was the setAttributedText: overload. The TextStorage was re-created every time you changed the label's text, and it looks like the layout manager didn't like that at all. Reusing the textStorage and just changing the text by calling setAttributedString: fixed the issue.

In fact, I solved it quite a long ago :( I edited the post, but forgot to answer myself. Sorry!