How to calculate if NSString is too long for drawInRect - ios

I am adding a variaty of different lengths to a pdf I am creating and I used the following code to write txt to the pdf.
if (subLocationCellTextString.length > 0) {
NSString *areaLocationString = #"Area Location:";
[areaLocationString drawInRect:CGRectMake(290.0, 195.0, 100.0, 25.0) withAttributes:blackAttributes];
// line
[self drawLineDarkGreyRect:290.0 StartB:215.0 FinishA:555.0 FinishB:215.0];
[subLocationCellTextString drawInRect:CGRectMake(382.0, 195.0, 200.0, 20.0) withAttributes:grayAttributes];
}
There are a few occasions where subLocationCellTextString is too long for the drawInRect that I make. I would like to know if there is a way to calculate the size of the NSString so if it is too large then I can wordwrap to a second line.

You can use it like
CGRect rect = [areaLocationString boundingRectWithSize:CGSizeMake(fixedWidth,CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:blackAttributes context:nil];
if (rect.size.height > yourheight){
//Too long
}

Related

How to get string height using width and font size?

I need to know string height so I can set cell height. The problem is I am getting string asynchronous, and I need to know cell Height before creating collection view. And I need this to work with different languages (special characters).
String will be in a label inside the cell. I had some of my solutions, but one did not work with other languages, and the other did work but didnt get the real heigth.
CGRect rect =[yourLabeltextString boundingRectWithSize:CGSizeMake(yourLabel.width, CGFLOAT_MAX)
options:(NSStringDrawingUsesFontLeading|NSStringDrawingUsesLineFragmentOrigin)
attributes:#{NSFontAttributeName:[UIFont fontWithName:#"Helvetica" size:17]}
context:nil];
CGFloat height=rect.height;
Use the code above, and change the attributes to the one you prefered.You can get the height of your String.
try this function and i hope it will help you :
- (CGFloat)calculateHeightForLabel:(UILabel *)label
{
CGRect frame = label.frame;
CGSize size = [label sizeThatFits:CGSizeMake(frame.size.width, FLT_MAX)];
return size.height;
}
use it o Swift 5
extension UILabel {
func calculateHeight() -> CGFloat {
let size = self.sizeThatFits(CGSize(width: self.bounds.width, height: CGFloat.greatestFiniteMagnitude))
return size.height
}
}

UITextView exclusionPaths bottom right

I am trying to do something whatsapp like for messages for having a cell that resizes based on contentsize and in the bottom right corner to have the date... the problem is that I don't know the rectangle position for the excluded path because the text is dynamic and I have something like this but works only for multiple lines, but not for a single line:
NSString *txt=#"I'm writing ";//a text renderer using Core Text, and I discovered I’ll need to wrap text around objects (such as is done in any DTP program). I couldn’t find any easy answers as to how to do this in the documentation, so having finally got it working I’ll share what I did. To lay out text in a custom shape in Core Text, you can pass a CGPath in when you create your CTFramesetterRef. Originally this only supported rectangular paths, but now it supports fully custom paths. My first thought was to see if I could subtract the region to wrap around from the path for my frame’s border, and pass the result in to Core Text as a path. It turns out firstly that subtracting one path from another in Core Graphics is not trivial. However, if you simply add two shapes to the same path, Core Graphics can use a winding rule to work out which areas to draw. Core Text, at least as of iOS 4.2, can also use this kind of algorithm. This will work for many cases: if you can guarantee that your object to be wrapped will be fully inside the frame (and not overlapping the edge), just go ahead and add its border to the same path as your frame, and Core"; // Text will do the rest.";
txtText.text=txt;
CGSize textFrame = [txtText sizeThatFits:CGSizeMake(235, MAXFLOAT)];
UIBezierPath * imgRect = [UIBezierPath bezierPathWithRect:CGRectMake(textFrame.width-35, textFrame.height-20, 35, 20)];
txtText.textContainer.exclusionPaths = #[imgRect];
textFrame = [txtText sizeThatFits:CGSizeMake(235, MAXFLOAT)];
//int frameWidth=375;
txtText.frame=CGRectIntegral(CGRectMake(10, 10, textFrame.width, textFrame.height));
lblHour.frame=CGRectIntegral(CGRectMake(textFrame.width-35, textFrame.height-15, 35, 20));
viewCanvasAround.frame=CGRectIntegral(CGRectMake(50, 100, textFrame.width+20, textFrame.height+20));
I had the same problem too. I added attributedString with textAttachment which it has a size. You will set an estimated maximum size. There's no problem with a single line either.
(Sorry, I am not good at English.)
This is the sample code.
var text: String! {
didSet {
let font = UIFont.systemFont(ofSize: 13);
let att = NSMutableAttributedString.init(string: text, attributes: [NSAttributedString.Key.foregroundColor : UIColor.black,
NSAttributedString.Key.font : font]);
let attachment = NSTextAttachment.init(data: nil, ofType: nil);
var size = extraSize;
size.height = font.lineHeight;
attachment.bounds = CGRect.init(origin: CGPoint.init(), size: size);
att.append(NSAttributedString.init(attachment: attachment));
textView.attributedText = att;
}
}
There is a nice solution my colleague found in actor.im. https://github.com/actorapp/actor-platform/blob/master/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleTextCell.swift#L312
let container = YYTextContainer(size: CGSizeMake(maxTextWidth, CGFloat.max))
textLayout = YYTextLayout(container: container, text: attributedText)!
// print("Text Layouted")
// Measuring text and padded text heights
let textSize = textLayout.textBoundingSize
if textLayout.lines.count == 1 {
if textLayout.textBoundingSize.width < maxTextWidth - timeWidth {
//
// <line_0> <date>
//
bubbleSize = CGSize(width: textSize.width + timeWidth, height: textSize.height)
} else {
//
// <line_________0>
// <date>
//
bubbleSize = CGSize(width: textSize.width, height: textSize.height + 16)
}
} else {
let maxWidth = textSize.width
let lastLine = textLayout.lines.last!.width
if lastLine + timeWidth < maxWidth {
//
// <line_________0>
// <line_________1>
// ..
// <line_n> <date>
//
bubbleSize = textSize
} else if lastLine + timeWidth < maxTextWidth {
//
// |------------------|
// <line______0>
// <line______1>
// ..
// <line______n> <date>
//
bubbleSize = CGSize(width: max(lastLine + timeWidth, maxWidth), height: textSize.height)
} else {
//
// <line_________0>
// <line_________1>
// ..
// <line_________n>
// <date>
//
bubbleSize = CGSize(width: max(timeWidth, maxWidth), height: textSize.height + 16)
}
}
I had the same problem (for me it was a button in right corner) and there is the way how I resolved it:
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self updateTextExclusionPaths];
}
- (void)updateTextExclusionPaths {
// Remove old exclusionPaths to calculate right rects of new
textView.textContainer.exclusionPaths = #[];
// Force textView to layout the text without exclusionPaths
[textView.layoutManager glyphRangeForTextContainer:textView.textContainer];
/// Because of some specifics of my implementation I had a problem with wrong frame of my button. So I calculated it manually
// Calculate new exclusionPaths
CGRect buttonFrameInTextView = [superViewOfTextView convertRect:button.frame toView:textView];
// Calculate textView.text size without any exclusionPaths to calculate right button frame
CGSize textViewSize = [textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)];
// Update Y position of button
buttonFrameInTextView.origin.y = size.height - buttonFrameInTextView.size.height;
// Add new exclusionPaths
UIBezierPath *buttonRectBezierPath = [UIBezierPath bezierPathWithRect:buttonFrameInTextView];
textView.textContainer.exclusionPaths = #[ buttonRectBezierPath ];
}

NSString boundingRectWithSize returning unnecessarily tall height

When using [NSString boundingRectWithSize:options:attributes] the size of the rect that is returned is taller than I would expect for certain strings. The height returned appears to represent the maximum possible height of a string with the given attributes, rather than the height of the string itself.
Assuming the same attributes and options, the height returned for the string "cars" is the same height returned for the string "ÉTAS-UNIS" (note the accent on the E).
I would have expected boundingRectWithSize to only consider the characters in the given string, which in my opinion would have it return a shorter height for the string "cars".
In the attached screenshots, I've filled the rect returned from boundingRectWithSize and outlined in red what I would have assumed the bounding rect should have been. The width of the rect is pretty much as I would expect but the height is considerably taller than I would have expected. Why is that?
Sample code:
NSRect boundingRect = NSZeroRect;
NSSize constraintSize = NSMakeSize(CGFLOAT_MAX, 0);
NSString *lowercaseString = #"cars";
NSString *uppercaseString = #"ÉTAS-UNIS";
NSString *capitalizedString = #"Japan";
NSFont *drawingFont = [NSFont fontWithName:#"Georgia" size:24.0];
NSDictionary *attributes = #{NSFontAttributeName : drawingFont, NSForegroundColorAttributeName : [NSColor blackColor]};
boundingRect = [lowercaseString boundingRectWithSize:constraintSize options:0 attributes:attributes];
NSLog(#"Lowercase rect: %#", NSStringFromRect(boundingRect));
boundingRect = [uppercaseString boundingRectWithSize:constraintSize options:0 attributes:attributes];
NSLog(#"Uppercase rect: %#", NSStringFromRect(boundingRect));
boundingRect = [capitalizedString boundingRectWithSize:constraintSize options:0 attributes:attributes];
NSLog(#"Capitalized rect: %#", NSStringFromRect(boundingRect));
Output:
Lowercase rect: {{0, -6}, {43.1953125, 33}}
Uppercase rect: {{0, -6}, {128.44921875, 33}}
Capitalized rect: {{0, -6}, {64.5, 33}}
You might want to use NSStringDrawingUsesDeviceMetrics in the options. From the docs:
NSStringDrawingUsesDeviceMetrics
Use the image glyph bounds (instead of the typographic bounds) when computing layout.
#omz still gets credit for making this work for me. His answer made me look around CoreText some more since I assume something like boundingRectWithSize ultimately calls a CoreText function.
In Session 226 from WWDC 2012 there was an entire section devoted to calculating the metrics of a line and, to my surprise, they talked about a new CoreText function called CTLineGetBoundsWithOptions.
As far as I can tell, that method is only documented in CTLine.h and in the CoreText Changes document. It certainly doesn't come up (for me) when doing a normal search in the documentation.
But it appears to work and in my testing it returns the exact same result as boundingRectWithSize for all the fonts installed on my system. Even better, is that it appears to be almost 2x faster than boundingRectWithSize.
As the WWDC video mentions, it's a bit obscure why you'd need to calculate the bounds of a string without taking things like line height into account, but if you do need to do that, then I think this might be the best method to use. As always, YMMV.
Rough sample code:
NSFont *font = [NSFont systemFontOfSize:13.0];
NSDictionary *attributes = #{(__bridge NSString *)kCTFontAttributeName : font};
NSAttributedString *attributeString = [[NSAttributedString alloc] initWithString:self.text attributes:attributes];
CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attributeString);
CGRect bounds = CTLineGetBoundsWithOptions(line, kCTLineBoundsUseGlyphPathBounds);
CFRelease(line);
return NSRectFromCGRect(bounds);
I tried the boundingRectWithSize function, but it didn't work for me.
What did work was sizeThatFits
Usage:
CGSize maxSize = CGSizeMake(myLabel.frame.size.width, CGFLOAT_MAX)
CGSize size = [myLabel sizeThatFits:maxSize];
Swift 3+ solution based on omz's answer:
let string: NSString = "test"
let maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: .greatestFiniteMagnitude)
let boundingRect = string.boundingRect(with: maxSize, options: .usesDeviceMetrics, attributes: <string attributes>)

Getting the Y coordinate for every occurrence of a particular string in a UITextView for line numbering

I'm trying to get the y positions of every \n character in a UITextView so I can align line numberings alongside the UITextView for accurate line numbers. What I'd like to do is get an array of the Y coordinates of every occurrence of \n.
At the moment, I can get the CGSize for the current word, but I'm not quite sure how I can adjust this in order to get the Y coordinate of every occurence of a particular word.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
NSRange aRange = self.editorText.selectedRange;
if(range.location<self.editorText.text.length)
{
NSString * firstHalfString = [self.editorText.text substringToIndex:range.location];
NSString *temp = #"s";
CGSize s1 = [temp sizeWithFont:[UIFont systemFontOfSize:15]
constrainedToSize:CGSizeMake(self.view.bounds.size.width - 40, MAXFLOAT) // - 40 For cell padding
lineBreakMode:UILineBreakModeWordWrap]; // enter you textview font size
CGSize s = [firstHalfString sizeWithFont:[UIFont systemFontOfSize:15]
constrainedToSize:CGSizeMake(self.view.bounds.size.width - 40, MAXFLOAT) // - 40 For cell padding
lineBreakMode:UILineBreakModeWordWrap]; // enter you textview font size
//Here is the frame of your word in text view.
NSLog(#"xcoordinate=%f, ycoordinate =%f, width=%f,height=%f",s.width,s.height,s1.width,s1.height);
CGRect rectforword = CGRectMake(s.width, s.height, s1.width, s1.height);
// rectforword is rect of yourword
}
else
{
// Do what ever you want to do
}
return YES;
}
The main part I'm not sure about is getting each occurrence of the character in such a way that I can retrieve its coordinates.
Is there a way to do this? Or is there an easier way for me to implement line numbering?
Is custom parsing your NSString and appending #) to the start of each line an option?
It's a non-trivial bit of string parsing and manipulating to accomplish, but it has the added benefit of not having to care at all about yCoordinates and other messy layout based parameters.

Dealing with points or pixels when using UIFont

I was trying to position a string in the vertical center of a CGRect in my drawRect:(CGRect) method.
The CGRect has this size:
CGRect rect = CGRectMake(0.0f, 0.0f, rectWidth, rectHeight);
To draw the string so that it is centered I tried this first:
CGFloat diffHeightRectAndFont = rectHeight - font.capHeight;
[str drawAtPoint:CGPointMake(0.0f, diffHeightRectAndFont * 0.5f) withFont:font];
This I first assumed would find the difference in height between my rect and font, then offsetting the font by half this height should give a y position in the "sweet spot".
However, the capHeight is in points and the rectHeight in pixels, so this solution kind of worked for fonts in the size range 12-15. After that the difference started to position the string outside the rect.
I went over this a few times and the only way to consistently position the string correctly turned out to be this 'hack', which is valid but does not do wonders for the readability of the code:
CGFloat diffHeightRectAndFont = rectHeight - [[NSString stringWithString:#"F"] sizeWithFont:font].height;
Is there a more direct way of obtaining the pixel height of a capital letter using a specific font?
Thank you in advance:)
I know its a bit late. but nevertheless my answer my help someone else.
NSString has a sizeWithFont: method (documented here) that I think can be used for this. It returns a CGSize structure, so you could do something similar to the following to find the height of the text inside your label.
CGSize textSize = [[label text] sizeWithFont:[label font]];
CGFloat heightOfStringWithSpecifiedFont = textSize.height;
UILabel has a font property that you can use to dynamically get the font details for your label as I'm doing above.
Hope this helps :)

Resources