Calculating proper size of label for given string - ios

I have problem with calculating rect from given string.
Here are my strings:
_name = #"Any veryveryveryvery key here:";
_value = #"Any very very very very long value";
and my layoutSubviews method:
- (void)layoutSubviews {
[super layoutSubviews];
CGRect rect = self.contentView.bounds;
CGFloat verticalMargin = 5.0f;
//calculate rect for given name string:
CGFloat halfScreen = CGRectGetMidX(rect); //160points in portrait
//_nameLabel:
CGRect nameRect =
[_name boundingRectWithSize:CGSizeMake(halfScreen, CGRectGetHeight(rect))
options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
attributes:#{NSFontAttributeName: [UIFont fontWithName:kHelveticaFont size:_nameLabel.font.pointSize]}
context:nil];
//name rect = origin=(x=0, y=0) size=(width=147.53174, height=34.5) - WHY width = 147.53174 NOT halfScreen = 160points?
[_nameLabel setFrame:CGRectMake(0.0f,
verticalMargin,
ceilf(CGRectGetWidth(nameRect)),
CGRectGetHeight(rect) - verticalMargin * 2)];
//calculate rect for given value string:
//_valueLabel:
CGFloat maxWidth = CGRectGetWidth(rect) - CGRectGetMaxX(_nameLabel.frame); //172points in portrait
CGRect valueRect =
[_value boundingRectWithSize:CGSizeMake(maxWidth, CGRectGetHeight(rect))
options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
attributes:#{NSFontAttributeName: [UIFont fontWithName:kHelveticaFont size: _valueLabel.font.pointSize]}
context:nil];
//valueRect = origin=(x=0, y=0) size=(width=160.03418, height=34.5) - WHY width = 160.03418 NOT maxWidth = 172 points?
[_valueLabel setFrame:CGRectMake(CGRectGetMaxX(_nameLabel.frame),
verticalMargin,
ceilf(CGRectGetWidth(valueRect)),
CGRectGetHeight(rect) - verticalMargin * 2)];
}
And result:
Yellow background is half Width of Screen (160 points). Green background is another 160 points. I want to have _nameLabel with max size equal to half screen width.
Anyone has ANY idea how to calculate it properly? Question are also commented in code. Thank you in advance for help.
NOTE: I'm developing an app only for iOS7.
EDIT:
Note, that if _name will be much shorter, for example "key", i want to have _valueLabel next to _nameLabel like this (and it works as it should):

I solved my problem. Actually I dived deep in apple documentation and the problem was generated by this argument:
NSStringDrawingUsesLineFragmentOrigin // which means: The specified origin is the line fragment origin, not the base line origin
in line:
options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
Thats why method:
(CGRect)boundingRectWithSize:options:attributes:context:
gave me wrong rect dimensions. Now everything works as should.

_valueLabel.text = _value;
CGSize fitSize = [_valueLabel sizeThatFits:CGSizeMake(FLT_MAX, 18.0)];
CGRect newFrame = _valueLabel.frame;
newFrame.size.width = if fitSize.width > 160.0 ? 160.0 : fitSize.width
_valueLabel.frame = newFrame;
Obviously your code has a few more calculations to move the second UITextField over but hence use the same fitSize above to calculate the origin of the second UITextField plus your margin. Hope that helps get you your issue solved.

Related

Single line UILabel hugging not working with autoshrink

I've a UILabel with the following properties:
A very big font
Fixed left margin to superview
Fixed right margin to superview
Centered vertically
Autoshrinks to a minimum font size to avoid truncating the text as much as possible.
Single line
Vertical hugging priority set to Required (1000)
The problem I'm having is that the label doesn't vertically hug the text, as you can see in the image bellow there's a lot of space on top and bellow the text.
I uploaded the sample project here.
Thanks!
Try the following code :
[self.myLabel setAutoresizingMask:UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleWidth];
I was able to achieve the desired effect with a workaround. It's not the perfect solution and doesn't work on all cases but it did solve my issue at least.
SUMMARY
The workaround works as follows:
Find the actual font (the font that is being actually used by the label).
Calculate the text height with the actual font.
Use UIViews's - (UIEdgeInsets)alignmentRectInsets (reference) to modify alignment rect that is used by Autolayout to position views based on the actual text height.
With all this, I was able to go from this:
to this:
PROBLEMS
Need to set baseline to center
Only implemented for exceeding height (doesn't work for right and left margins)
Only for single line labels
CODE
Declared a UILabel subclass as follows
#implementation HuggingLabel
- (UIEdgeInsets)alignmentRectInsets
{
if (self.numberOfLines != 1)
{
if ([super respondsToSelector:#selector(alignmentRectInsets)])
return [super alignmentRectInsets];
else
return UIEdgeInsetsZero;
}
UIFont *fontThatFitsBounds = [self fontThatFitsBounds];
CGSize textSize = [self.text sizeWithAttributes:#{
NSFontAttributeName : fontThatFitsBounds
}];
CGSize actualSize = self.bounds.size;
CGFloat exceedingWidth = actualSize.width = textSize.width;
CGFloat exceedingHeight = actualSize.height - textSize.height;
CGFloat exceedingHeightTop = exceedingHeight / 2.0f;
CGFloat exceedingHeightBottom = MAX(0.0f, exceedingHeight / 2.0f + fontThatFitsBounds.descender);
UIEdgeInsets insets = UIEdgeInsetsMake(
exceedingHeightTop,
0.0f,
exceedingHeightBottom,
0.0f
);
return insets;
}
- (UIFont *)fontThatFitsBounds
{
CGSize currentSize = CGSizeZero;
CGFloat fontSizeThatFits = self.font.pointSize + 1.0f;
do {
fontSizeThatFits = fontSizeThatFits - 1.0f; //try with one point smaller size
NSDictionary *attributes = [self stringAttributesWithFontOfSize:fontSizeThatFits];
currentSize = [self.text sizeWithAttributes:attributes];
} while (currentSize.width > self.bounds.size.width ||
currentSize.height > self.bounds.size.height);
return [self.font fontWithSize:fontSizeThatFits];
}
- (NSDictionary *)stringAttributesWithFontOfSize:(CGFloat)fontSize
{
NSMutableParagraphStyle * paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.lineBreakMode = self.lineBreakMode;
paragraphStyle.alignment = self.textAlignment;
NSDictionary *attributes = #{
NSFontAttributeName : [self.font fontWithSize:fontSize],
NSParagraphStyleAttributeName : paragraphStyle
};
return attributes;
}
#end
Demo project can be downloaded for here

Set proper gap between text.view frame and button frame

I have a text view and buttons. For some reason i got unexpected result when trying to set proper gap between those 2 elements. I think there could be incorrect calculating of text.view frame. That is how i manage to set text.view frame:
//1 Setting properties for text, starting with dictionary containing font size and family
NSDictionary *stringAttributes = [NSDictionary dictionaryWithObject:[UIFont fontWithName:#"helvetica neue"
size:14]
forKey: NSFontAttributeName];
//Creating frame to hold textView
CGSize maximumLabelSize = CGSizeMake(self.myTextView.frame.size.width, FLT_MAX);
CGSize textViewSize = [self.descriptionStringShort boundingRectWithSize:maximumLabelSize
options:NSStringDrawingTruncatesLastVisibleLine|NSStringDrawingUsesLineFragmentOrigin
attributes:stringAttributes context:nil].size;
//Setting frame
CGRect frame = self.myTextView.frame;
frame.size.height = textViewSize.height;
NSLog(#"%f height", frame.size.height);
self.myTextView.frame = frame;
After that, when tried to set button frames it always show different gap between those to elements (my "rough" solutions is to check height of text.view and set "minimum" and "maximum" y depend on it, but it won't work in a real world app). For example, button frame may be draw on textView.frame or much below it. There is how i set map frame:
CGRect mapButtonFrame = self.mapButton.frame;
mapButtonFrame.origin.x = 30;
if (frame.size.height < 50){
mapButtonFrame.origin.y = (200+frame.size.height + 150);
} else if (frame.size.height >300){
mapButtonFrame.origin.y = (200+frame.size.height +50);
} else {
mapButtonFrame.origin.y = (200+frame.size.height +100);
}
self.mapButton.frame = mapButtonFrame;
Still, that approach is bad, please take a look at screenshots attached:
Here, as you can see, always different gap between 2 elements. How could i make it fixed?
By the way, auto layout is turned off.
Any advice would be appreciated, thanks1
This should work just fine:
[myTextView sizeToFit];//This will change the textView frame according to the text length
CGRect bFrame = self.mapButton.frame;
bFrame.origin.y = myTextView.frame.size.height + 20.0f;// The gap you want between the text and the button
self.mapButton.frame = bFrame;

boundingRectWithSize not replicating UITextView

My requirement in a project is that the font size of the UITextView should decrease according the content of the UITextView. So i am trying to do estimate the size of the text using boundingRectWithSize.
The problem is that the font size I get is a bit too big and some part of the text does get clipped.
My Function :
-(BOOL)updateTextViewFontSizeForText:(NSString*)text{
float fontSize = self.maximumFontSizeInPoints;
self.font = [self.font fontWithSize:fontSize];
CGSize tallerSize ;
CGSize stringSize ;
do
{
if (fontSize <= self.minimumFontSizeInPoints) // it just won't fit
return NO;
fontSize -= 1.0;
self.font = [self.font fontWithSize:fontSize];
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
[paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping];
NSDictionary *attributes = #{ NSFontAttributeName: self.font, NSParagraphStyleAttributeName : paragraphStyle };
tallerSize = CGSizeMake(self.frame.size.width,self.frame.size.height-16);// the 16 is given because uitextview adds some offset
stringSize = [text boundingRectWithSize:CGSizeMake(self.contentSize.width,CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading attributes:attributes context:nil].size;
}while(stringSize.height >= tallerSize.height);
if ([self.onTextChangDelegate respondsToSelector:#selector(onTextChangDelegate)]) {
[self.onTextChangDelegate onTextChanged:text];
}
return YES;
}
I ran into the same issue when trying to do the same thing.
The issue is how UITextView run's its line-breaks compared to boundingRectWithSize. You can read more details here: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/TextLayout/Concepts/CalcTextLayout.html
But you can actually calculate the exact size! There are basically two properties of a UITextView that you'll need to take into account in order to get correct size estimates. The first is textContainer.lineFragmentPadding, the second is textContainerInset.
First, textContainer.lineFragmentPadding: You may have noticed that your sizing is generally always off by 10px, this is because the systems default value is 5px. When you're calculating your estimated size, you'll need to subtract this value from the size you're checking against and add it back when you have your final value.
Second, textContainerInset. This is a UIEdgeInset that you'll need to add back to your final calculated value to match the systems.
This is code based on how I solved the issue:
- (CGSize)sizeThatFits:(CGSize)size
CGFloat lineFragmentPaddings = self.textContainer.lineFragmentPadding * 2;
CGFloat horzPadding = self.textContainerInset.left + self.textContainerInset.right + lineFragmentPaddings;
CGFloat vertPadding = self.textContainerInset.top + self.textContainerInset.bottom;
size.width -= horzPadding;
CGRect boundingRect = [attributedText boundingRectWithSize:size options:NSStringDrawingUsesLineFragmentOrigin context:nil];
size = boundingRect.size;
// I found through debugging that adding 0.25 rounded
// matches sizeThatFits: identically. Not sure why…
size.width += horzPadding + 0.25;
size.height += vertPadding + 0.25;
size = CGSizeRound(size);
return size;
}
Note, CGSizeRound is just a custom function I wrote that rounds the width and height of the CGSize to the nearest 0.5.
For comparison, if you create a second UITextView, and make sure the textContainer.lineFragmentPadding and textContainerInset are the same, you should see the values almost identical to the nearest 0.5.
And to your question about calculating a proper pointSize, this is some pseudo code for that:
CGFloat pointSize = 64;
CGFloat minPointSize = 32;
CGFloat decrementor = 4;
CGFloat padding = self.textContainerInset.left + self.textContainerInset.right + lineFragmentPaddings;
CGFloat actualWidth = self.maxTextViewSize.width - padding * 2;
CGRect boundingRect = CGRectZero;
BOOL isValidPointSize = NO;
do {
if (pointSize < minPointSize) {
pointSize = minPointSize;
boundingRect.size.height = self.maxTextViewSize.height;
isValidPointSize = YES;
} else {
NSDictionary *defaultAttributes = [self.customTextStorage defaultAttributesForPointSize:pointSize];
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:defaultAttributes];
boundingRect = [attrString boundingRectWithSize:CGSizeMake(actualWidth, 1024) options:NSStringDrawingUsesLineFragmentOrigin context:nil];
// is the height to big?
if (boundingRect.size.height > self.maxTextViewSize.height) {
// reduce the point size for next iteration of loop
pointSize -= decrementor;
}
// passes height test
else {
isValidPointSize = YES;
}
}
} while (!isValidPointSize);
return pointSize;
Again, the above is pseudo code based on my implementation (not meant for just drop in replacement for what you have). Hope this helps!
try like this
UITextView *textViewObj;//initialise textview.
textViewObj.autoresizesSubviews = NO;
textViewObj.autoresizingMask = UIViewAutoresizingNone;
This is really working in swift,get original height of textview.. try this
let
size = cellQueue.contentLbl.sizeThatFits(CGSizeMake(cellQueue.contentLbl.frame.size.width,CGFloat(MAXFLOAT))) cellQueue.heightConstraintContentLbl.constant = size.height

Calculate NSString size to adjust UITextField frame

I have issues calculating the accurate size of a NSString displayed in a UITextField.
My goal is to update the textfield frame size according to the string size programmatically (without using sizeToFit). I am using the sizeWithFont function.
-(void)resizeTextFieldAccordingToText:(NSString*)textFieldString {
CGPoint originalCenter = self.textField.center;
UIFont* currentFont = [textField font];
CGSize newSize = [textFieldString sizeWithFont:currentFont];
//Same incorrect results with the extended version of sizeWithFont, e.g.
//[textFieldString sizeWithFont:currentFont constrainedToSize:CGSizeMake(300.0, 100.0) lineBreakMode:NSLineBreakByClipping];
[self.textField setFrame:(CGRectMake(self.textField.frame.origin.x, self.textField.frame.origin.y, newSize.width, newSize.height))];
[self.textField setCenter:originalCenter];
}
Problem: While this return correct size results at first its becomes more and more unprecise by adding characters therefore finally starts clipping the string (as seen in the right screenshot).
How do I get the accurate size of the textField string for correctly adjusting its size?
UITextField has it's own layout inside if you use borderStyle != UITextBorderStyleNone. In this case you have to increase text size dimensions by some constants.
With UITextBorderStyleNone you don't have this problem, and code below works like a charm (iOS 7 introduced new method to get text size, -sizeWithFont: is deprecated)
- (IBAction)textChanged:(UITextField *)field
{
UIFont *font = field.font;
NSString *string = field.text;
CGSize size = [string sizeWithAttributes:
[NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName]];
CGPoint center = field.center;
CGRect frame = field.frame;
frame.size = size; // or CGSizeMake(size.width + WIDTH_PADDING * 2, size.height + HEIGHT_PADDING * 2)
field.frame = frame;
field.center = center;
}
the problem is that you don't take into account the contentInset of the UITextField. Your code would be fine for a label not for a textfield.
for example: one way could be:
CGPoint originalCenter = self.textField.center;
UIFont* currentFont = [textField font];
CGSize oldSize = [self.textField.text sizeWithFont:currentFont];
CGSize newSize = [textFieldString sizeWithFont:currentFont];
CGRect finalFrame = self.textField.frame
finalFrame.size.width -= oldSize.width;
finalFrame.size.width += newSize.width;
finalFrame.size.height -= oldSize.height;
finalFrame.size.height += newSize.height;
[self.textField setFrame:finalFrame];
[self.textField setCenter:originalCenter];
ios7 deprecates sizeWithFont:currentFont so it is sizeWithAttributes:#{NSFontAttributeName:currentFont}

Why can't I change the frame of my UILabel?

I have been trying for two days to modify the frame of a UILabel, which
is ridiculous... The UILabel is an IBOutlet, but that is not the reason
why it's not working: I tried to create a UILabel programatically and
it still didn't work. Here is how I do it:
self.descriptionLabel.text = description;
self.descriptionLabel.lineBreakMode = NSLineBreakByWordWrapping;
CGSize textSize = [self.descriptionLabel.text sizeWithFont:[UIFont systemFontOfSize:12.0]
constrainedToSize:CGSizeMake(self.descriptionLabel.frame.size.width, FLT_MAX)
lineBreakMode:self.descriptionLabel.lineBreakMode];
CGFloat frameHeight = textSize.height;
CGRect frame = self.descriptionLabel.frame;
frame.size.height = frameHeight;
self.descriptionLabel.frame = frame;
CGRect bounds = self.descriptionLabel.frame;
bounds.origin.x = self.descriptionLabel.frame.origin.x + 10.0;
bounds.size.width = self.descriptionLabel.frame.size.width - 20.0;
self.descriptionLabel.bounds = bounds;
self.descriptionLabel.numberOfLines = 0;
I already asked on IRC and they told me that there is no reason why the
frame would not be changed...
I also tried to create a frame with CGRectMake and give it arbitrary
value but that didn't do the trick either..
Does anyone know what might be the problem please?
Edit 1: I logged the frame before and after, and I got odd results:
Before: 0.000000 0.000000 0.000000 0.000000
After: 0.000000 0.000000 0.000000 33525.000000
The height after I set the frame is the only value that makes sense (I have a lot of text in the label on purpose).
Edit 2: I changed the code as follows, the log says that the frame
changed but on the simulator it didn't change; I proved it by adding red
borders to the layer of the UILabel.
self.descriptionLabel.text = description;
self.descriptionLabel.lineBreakMode = NSLineBreakByWordWrapping;
CGSize textSize = [self.descriptionLabel.text sizeWithFont:[UIFont systemFontOfSize:12.0]
constrainedToSize:CGSizeMake(self.view.frame.size.width, FLT_MAX)
lineBreakMode:self.descriptionLabel.lineBreakMode];
CGFloat frameHeight = textSize.height;
CGRect frame = CGRectMake(0.0, 300.0, self.view.frame.size.width, frameHeight);
self.descriptionLabel.frame = frame;
DDLogInfo(#"After: %f %f %f %f", self.descriptionLabel.frame.origin.x, self.descriptionLabel.frame.origin.y, self.descriptionLabel.frame.size.width, self.descriptionLabel.frame.size.height);
// After: 0.000000 300.000000 320.000000 585.000000
self.descriptionLabel.numberOfLines = 0;
self.descriptionLabel.layer.borderColor = [UIColor redColor].CGColor;
self.descriptionLabel.layer.borderWidth = 2;
If you're using auto layout, you shouldn't be doing any setting of frames -- either turn off auto layout, or use constraints to change the size of your label. The easiest way to do this is to give your label a height constraint (and width too, if you want a constant width) in IB, and make an IBOutlet to it. Then, in code, change the constant value of that constraint based on the value you get from sizeWithFont:constrainedToSize:lineBreakMode:.
Your label has a zero frame to begin with, so, when you do this:
CGSize textSize = [self.descriptionLabel.text sizeWithFont:[UIFont systemFontOfSize:12.0]
constrainedToSize:CGSizeMake(self.descriptionLabel.frame.size.width, FLT_MAX)
lineBreakMode:self.descriptionLabel.lineBreakMode];
You're asking the sizeWithFont method to do something effectively impossible - tell you the height that is required when no width is allowed. The response it provides (33525.000000) is probably the height if one character is allowed on each line or something like that.
So, either set the width of the label before you start, or provide the allowed width to the sizeWithFont method and then set the label size using the response (not just the height).
Also, consider what you're trying to do by changing the bounds. Comment that code out till you get the frame right at least, then you can add it back in and check it actually does what you want.

Resources