NSTextView and determining glyph position - objective-c

I am trying to define y axis position for certain lines and have label next to them in an NSTextView. I am using glyphRangeForCharacterRange.
It kind of works, but the text view can have a LOT of text (even ~4000-5000 lines) and is wrapped with insets. As I scroll to stuff that is out of view, the results are wrong. It seems to return values without wrapping, or so I figure. If I do an update on the positions while I have scrolled to corresponding text, they are calculated correctly.
This is the code without additional offsets:
NSRange characterRange = NSMakeRange(startPosition, length);
NSRange range = [[self.textView layoutManager] glyphRangeForCharacterRange:characterRange actualCharacterRange:nil];
label.frame = rect;
NSRect rect = [[self.textView layoutManager] boundingRectForGlyphRange:range inTextContainer:[self.textView textContainer]];
The same problem occurs if I try to set the label positions after the document awakes, so I calculate the labels on first keypress, which isn't too nice.
What would be the best way around this?

EDIT
The answer to this problem was just to ensure the layout for the NSLayoutManager before retrieving glyph coordinates.
[[_textView layoutManager] ensureLayoutForTextContainer:[_textView textContainer]];

Related

How to find the position of the last text line in a multiline UILabel or otherwise have UILabel have 0 padding

I have a UILabel that has both -numberOfLines set to 3 and text-size auto shrink and I need to align another UIView to this UILabel's last line of text. That is, I might need to align to the y position of line 0, 1 or 2, depending on the text inside the label (and the distance between these lines of text may vary depending on whether the text is long enough that it triggered font resizing).
But:
UILabel doesn't expose a contentSize
the label's bounds extend past the last line of text (there seems to be a content inset), so aligning to the bounds won't work.
subclassing UILabel and doing something like this:
- (void)drawTextInRect:(CGRect)rect {
UIEdgeInsets insets = {0., 0., -30., 0.};
return [super drawTextInRect:UIEdgeInsetsInsetRect(rect, insets)];
}
just happens to work for the case where I have 3 lines and the font size was auto shrunk, but I still can'r figure out a generic way of subtracting insets for the general case, regardless of text size. And I don't seem to be able to use -boundingRectWithSize:options:context: either: it either returns a single line equivalent rect or, If I play around with the options, a a rect the same size of the original label (that is, including the extra insets I'm trying to get rid of). Mind you, the idea behind removing any insets is that if I have no way of knowing where the last line of text is, at least I can remove any insets in the label so that the last line of text aligns with the label's bounds.origin.y + bounds.size.height.
Any thoughts?
I don't know if the problem was that originally I was using boundingRectWithSize on non-attributed text or what but now this seems to work:
NSString *text = <get text from CoreData>;
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:#{NSFontAttributeName: self.titleLabel.font}];
CGRect rect = [attributedText boundingRectWithSize:self.titleLabel.frame.size
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
if (!rect.size.height || rect.size.height > self.titleLabel.frame.size.height) {
attributedText = [[NSAttributedString alloc] initWithString:text attributes:#{NSFontAttributeName: [UIFont boldSystemFontOfSize:self.titleLabel.font.pointSize * self.titleLabel.minimumScaleFactor]}];
rect = [attributedText boundingRectWithSize:self.titleLabel.frame.size
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
}
self.titleLabel.frame = rect;
self.titleLabel.attributedText = attributedText;
While this doesn't really find the position of the bottom of the last line of text in the UILabel (the label still adds some padding at the bottom... not sure if to account for descenders), it adjusts the label's bounds close enough to the bottom that I can at least align based on bounds.origin.y + bounds.size.height and it looks good enough.

How to fit a text with various length in a UITableViewCell?

What I have is:
a NSString which can have any length between 1 and 400 characters
a UITableViewCell (custom layout)
I tried using an UILabel with multiple lines, set the text, and call sizeToFit. That doesn't work always, most of the time the UILabel just clips off the part of the string that doesn't fit. Also, due the varying length of the text I'd need differently sized UITableViewCells, and at the time "tableView: cellForRowAtIndexPath:" is called I don't know what the height will be.
So what I need is a non-scrolling UI element which is able to display text and resizes its height (the width should remain constant) to exactly fit the text. As mentioned the sizeToFit method produces mostly garbage.
You can use SizeWithFont: to calculate the desired height for your cell and store it in an Array so that you can return that height in HeightForRowAtIndexPath. If you need to update the text, just have a method that re-calculates the height, saves it to the array, and updates the table. Something like:
CGSize constraintSize;
constraintSize.width = 290.0f;
constraintSize.height = MAXFLOAT;
NSString *text = #"YOUR TEXT"
CGSize theSize = [text sizeWithFont:[UIFont systemFontOfSize:15.0f] constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];
NSLog(#"height: %f",theSize.height);
will give you the height.
This configuration should give you something simillar to what you see when you enter a loooong number in the phone app -
label.minimumFontSize = 4; //a very small font size
label.adjustsFontSizeToFitWidth = YES;
label.lineBreakMode = UILineBreakModeWordWrap;// change to what works for you
label.numberOfLines = 0;
See lineBreakMode Documentation

find the location {x,y} of text in uilabel

I have a string coming from server which I am displaying on UILabel multiligne. It is within that string, I am identifying some particular substring. I want to place a button on that substring(button will be a subview of UILabel). For this I require substring coordinates. I went through this but I am not able to understand it. Suppose my complete string is abc, 567-324-6554, New York. I want 567-324-6554 to be displayed on button for which I need its coordinates.
How can I use above link to find coordinates of substring?
Thanks,
UILabel doesn't have any methods for doing this. You can do it with UITextView, because it implements the UITextInput protocol. You will want to set the text view's editable property to NO.
Something like this untested code should work:
- (CGRect)rectInTextView:(UITextView *)textView stringRange:(CFRange)stringRange {
UITextPosition *begin = [textView positionFromPosition:textView.beginningOfDocument offset:stringRange.location];
UITextPosition *end = [textView positionFromPosition:begin offset:stringRange.length];
UITextRange *textRange = [textView textRangeFromPosition:begin toPosition:end];
return [textView firstRectForRange:textRange];
}
That should return a CGRect (in the text view's coordinate system) that covers the substring specified by stringRange. You can set the button's frame to this rectangle, if you make the button a subview of the text view.
If the substring spans multiple lines, the rectangle will only cover the first line.

How to make a UILabelView display text with new lines?

I have a UILabelView within a RoundedRectView. The UILabelView is supposed to show a dynamically generated text string with new lines in it.
The problem is that the UILabel still shows the text as "blahblahbla..." and does not seem to show the text over multiple lines. The only exception is when I do a "sizeToFit" function call however at that time, the text no longer appears in the center of the parent view. I have the UILabelView number of lines to be 0 and also have increased the frame of the parent view (RoundedRectView)
What can I do to get past this?
Here is my code:
CGRect frame = [self.messageView frame]; //get the frame size of parent view
self.messageView.frame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.width, (lineHeight * count)); //increase it to fit the text
self.messageLabel.text = message;
[self.messageView setNeedsDisplay];
// [self.messageLabel sizeToFit]; this fixes the multiple line issue but now label is not centered in parent view
Set numberOfLines to 0, lineBreakMode to UILineBreakModeWordWrap, adjustsFontSizeToFitWidth to NO. Check if your text contains '\n'.
[self.messageLabel setNumberOfLines:0];
Source: apple docs

Get Bounding Rectangle of String with NSLayoutManager

I have a large amount of unique strings for which I want to compute their bounding rectangle when they would be laid out in an infinitly large rectangle. Currently I use a single NSTextStorage/NSLayoutManager and loop over all strings, collecting the rectangles:
// setup NSTextStorage and its NSLayoutManager, NSTextContainer
...
forall (NSAttributedString *astring in ...)
{
// put string into textstorage
[textStorage setAttributedString:astring];
// trigger glyph generation and layout
[textContainer setContainerSize: NSMakeSize (CGFLOAT_MAX, CGFLOAT_MAX)];
[layoutManager ensureLayoutForTextContainer: textContainer];
// finally get the bounding box
NSRect boundingBox = [layoutManager usedRectForTextContainer: textContainer];
...
}
The question is: is it possible to speed up the computation considering that the strings don't need to be drawn? I'm only interested in the rectangle's width and height.
Jut answering this myself after a few days of testing: no, unfortunately there is no faster way using layout-managers. Using CoreText seems too be about twice as fast, but CoreText has some nasty problems itself.