Get line information from UITextView and NSLayoutManager
Asked Answered
A

1

12

In order to support the UIAccessibilityReadingContent protocol, I need my UITextView to answer me questions about its lines. These are the methods of the protocol that I need to implement:

  • accessibilityLineNumberForPoint: <- Provided a coordinate, return a line number
  • accessibilityContentForLineNumber: <- Return the text of a given line
  • accessibilityFrameForLineNumber: <- Given a line number, return its frame
  • accessibilityPageContent <- The entire text content. That I have. :)

I figure that NSLayoutManager can help me, but I'm not that experienced with it. I've figured some of it out (I think), but still need some help.

Apple has some sample code (here) that can get me the number of lines in the text view:

NSLayoutManager *layoutManager = [textView layoutManager];
unsigned numberOfLines, index, numberOfGlyphs =
        [layoutManager numberOfGlyphs];
NSRange lineRange;
for (numberOfLines = 0, index = 0; index < numberOfGlyphs; numberOfLines++){
    (void) [layoutManager lineFragmentRectForGlyphAtIndex:index
            effectiveRange:&lineRange];
    index = NSMaxRange(lineRange);
}

I figure that with lineRangeabove, I can calculate the line rects using this method on NSLayoutManager:

- (NSRect)boundingRectForGlyphRange:(NSRange)glyphRange inTextContainer:(NSTextContainer *)container

And given lineRanges I should be able to calculate the line number for a point using (by finding the lineRange that contains the glyph index:

- (NSUInteger)glyphIndexForPoint:(CGPoint)point inTextContainer:(NSTextContainer *)container fractionOfDistanceThroughGlyph:(CGFloat *)partialFraction

So what remains is, how do I get the content of a line (as an NSString), given a line number?

Apperceive answered 2/4, 2014 at 10:3 Comment(3)
Are you sure that you need that protocol, in common case you can just set NSTextStorage, NSLayoutManager and NSTextContainer and everything will be fine. I read great topic about it: raywenderlich.com/50151/text-kit-tutorial.Arlo
Unfortunately yes. UITextView doesn't seem to provide line by line reading out of the box when VoiceOver is enabled. At least not in my implementation (UITextViews embedded in UITableViewCells)Dedra
NSLayoutManager offers methods that work with glyph ranges, as well as methods for characer ranges. Finally there are two methods that allow you to convert between both of those ranges : -[NSLayoutManager glyphRangeForCharacterRange:actualCharacterRange:] and -[NSLayoutManager characterRangeForGlyphRange:actualGlyphRange:]. To retrieve the string, simply convert the glyphRange you are interested in into a characterRange, then you can retrieve a substring from the UITextView's textStorage using attributedSubstringFromRange: or both string and substringWithRange:.Bindle
E
7

thinking in an easy solution, here is a small code that reproduce what you need

- (void)analyse:(UITextView *)textView
{
    NSLayoutManager *layoutManager = [textView layoutManager];
    NSString *string = textView.text;
    unsigned numberOfLines, index, stringLength = [string length];

    NSMutableArray *ranges = [NSMutableArray new];
    NSMutableArray *frames = [NSMutableArray new];
    for (index = 0, numberOfLines = 0; index < stringLength; numberOfLines++)
    {
        NSRange tmprange;
        NSRange range = [string lineRangeForRange:NSMakeRange(index, 0)];
        CGRect rect = [layoutManager lineFragmentRectForGlyphAtIndex:index
                                           effectiveRange:&tmprange];

        [ranges addObject:[NSValue valueWithRange:range]];

        [frames addObject:[NSValue valueWithCGRect:rect]];
        index = NSMaxRange(tmpRange);
    }
    self.ranges = ranges;
    self.frames = frames;
    self.numberOfLines = numberOfLines;
}

Please take a look of the properties:

self.ranges = ranges;
self.frames = frames;
self.numberOfLines = numberOfLines;

You can have the following in your class to create this properties:

@property (nonatomic) NSInteger numberOfLines;
@property (strong, nonatomic) NSArray *ranges;
@property (strong, nonatomic) NSArray *frames;

I suggest you to add the analyse call inside the following delegate:

- (void)textViewDidChange:(UITextView *)textView

There you can for example after analyse get the frame of the 2nd line just doing: self.frames[1]

Or getting the text of the second line doing: [textView.text substringWithRange:[self.ranges[1] rangeValue]]

For example like this:

if (self.numberOfLines > 1)
{
    NSRange range = [self.ranges[1] rangeValue];
    NSLog(@"2nd line = %@", [textView.text substringWithRange:range]);
    NSLog(@"2nd line frame = %@", self.frames[1]);
}

Having all the frames in self.frames I think you can easily do the other thing, guess the line number using a coordinate.

Ellinger answered 28/4, 2015 at 2:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.