NSString font size specific to frame width - objective-c

I am using drawRect for a text display, calling NSString. I am trying to implement using sizeWithFont to auto resizing font (shrinking) with default font size of 17 and using a loop to reduce the font size by 1 if it does not fit the size of width. Can anyone help me how to implement this? Example would be nice right now I just have the font size set to 17.0
[[self.string displayName] drawAtPoint:CGPointMake(xcoord, ycoord) withFont:[UIFont boldSystemFontOfSize:17.0]];
CGSize size = [[self.patient displayName] sizeWithFont:[UIFont boldSystemFontOfSize:17.0]];
max_current_y = size.height > max_current_y ? size.height : max_current_y;
xcoord = xcoord + 3.0f + size.width;

OK never mind. Here's modified version of the same method that takes NSString for which to return a font:
-(UIFont*)getFontForString:(NSString*)string
toFitInRect:(CGRect)rect
seedFont:(UIFont*)seedFont{
UIFont* returnFont = seedFont;
CGSize stringSize = [string sizeWithAttributes:#{NSFontAttributeName : seedFont}];
while(stringSize.width > rect.size.width){
returnFont = [UIFont systemFontOfSize:returnFont.pointSize -1];
stringSize = [string sizeWithAttributes:#{NSFontAttributeName : returnFont}];
}
return returnFont;
}
Here's how to call it:
NSString* stringToDraw = #"Test 123";
CGRect rect = CGRectMake(100., 100., 100., 200.);
UIFont* font = [self getFontForString:stringToDraw toFitInRect:rect seedFont:[UIFont systemFontOfSize:20]];
[stringToDraw drawInRect:rect withFont:font];
Code is for iOS7+

Trying font sizes with step 1.0 may be very slow. You can tremendously improve the algorithm by making two measures for two different sizes, then using linear approximation to guess the size that will be very close to the right one.
If it turns out not close enough, repeat the calculation using the guessed size instead of one of the previous two until it is good enough or stops changing:
// any values will do, prefer those near expected min and max
CGFloat size1 = 12.0, size2 = 56.0;
CGFloat width1 = measure_for_size(size1);
CGFloat width2 = measure_for_size(size2);
while (1) {
CGFloat guessed_size = size1 + (required_width - width1) * (size2 - size1) / (width2 - width1);
width2 = measure_for_size(guessed_size);
if ( fabs(guessed_size-size2) < some_epsilon || !is_close_enough(width2, required_width) ) {
size2 = guessed_size;
continue;
}
// round down to integer and clamp guessed_size as appropriate for your design
return floor(clamp(guessed_size, 6.0, 24.0));
}
is_close_enough() implementation is completely up to you. Given that text width grows almost linearly of font size, you can simply drop it and just do 2-4 iterations which should be enough.

I wanted to try to make a version that didn't have to repeatedly check font sizes using a do...while loop. Instead, I assumed that font point sizes were a linear scale, then worked out the size difference between the required frame width and the actual frame width, then adjusted the font size accordingly. Therefore, I ended up with this function:
+ (CGFloat)fontSizeToFitString:(NSString *)string inWidth:(float)width withFont:(UIFont *)font
{
UILabel *label = [UILabel new];
label.font = font;
label.text = string;
[label sizeToFit];
float ratio = width / label.frame.size.width;
return font.pointSize * ratio;
}
Pass in a font of any size, as well as the string and the required width, and it will return you the point size for that font.
I also wanted to take it a bit further and find out the font size for a multi-line string, so that the longest line would fit without a line break:
+ (CGFloat)fontSizeToFitLongestLineOfString:(NSString *)string inWidth:(float)width withFont:(UIFont *)font
{
NSArray *stringLines = [string componentsSeparatedByString:#"\n"];
UILabel *label = [UILabel new];
label.font = font;
float maxWidth = 0;
for(NSString *line in stringLines)
{
label.text = line;
[label sizeToFit];
maxWidth = MAX(maxWidth, label.frame.size.width);
}
float ratio = width / maxWidth;
return font.pointSize * ratio;
}
Seems to work perfectly fine for me. Hope it helps someone else.

Original poster didn't specify what platform he was working on, but for OSX developers on Mavericks, sizeWithFont: doesn't exist and one should use sizeWithAttributes :
NSSize newSize = [aString sizeWithAttributes:
[NSDictionary dictionaryWithObjectsAndKeys:
[NSFont fontWithName:#"Arial Rounded MT Bold" size:53.0],NSFontAttributeName,nil
]];

Here's a method which can return you font that will fit in a rect:
-(UIFont*)getFontToFitInRect:(CGRect)rect seedFont:(UIFont*)seedFont{
UIFont* returnFont = seedFont;
CGSize stringSize = [self sizeWithFont:returnFont];
while(stringSize.width > rect.size.width){
returnFont = [UIFont systemFontOfSize:returnFont.pointSize -1];
stringSize = [self sizeWithFont:returnFont];
}
return returnFont;
}
You can add this method to a NSString category. You can find more about how to add a category here: http://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html#//apple_ref/doc/uid/TP40011210-CH6-SW2
If you don't want to create a category, you can add this method to one of your utility classes and pass in the string for which you want the font to be returned.

Here is another method, inspired by #puru020 & #jowie answers. Hope it helps someone
-(UIFont *) adjustedFontSizeForString:(NSString *)string forWidth:(float)originalWidth forFont:(UIFont *)font
{
CGSize stringSize = [string sizeWithFont:font];
if(stringSize.width <= originalWidth)
{
return font;
}
float ratio = originalWidth / stringSize.width;
float fontSize = font.pointSize * ratio;
return [font fontWithSize:fontSize];
}

I modified a bit the solution of #puru020 , added the support for attributes, and improved a bit:
Note: The method should be wrapped in a NSString Category
- (UIFont*)requiredFontToFitInSize:(CGSize)size seedFont:(UIFont*)seedFont attributes:(NSDictionary*)attributes{
UIFont *returnFont = [UIFont systemFontOfSize:seedFont.pointSize +1];
NSMutableDictionary *mutableAttributes = attributes.mutableCopy;
CGSize stringSize;
do {
returnFont = [UIFont systemFontOfSize:returnFont.pointSize -1];
[mutableAttributes setObject:returnFont forKey:NSFontAttributeName];
stringSize = [self sizeWithAttributes:mutableAttributes];
} while (stringSize.width > size.width);
return returnFont;
}

Related

Padding Spaces in a NSString

The following question is for Objective C preferably (Swift is fine too). How can I get my strings to look like the strings in the picture below? The denominators and the right bracket of the percentage portions need to line up. Obviously the percentages could be 100%, 0%, 0%, which means that the left bracket for the percentages wouldn't line up, which is fine. The amount of space that the percentage part requires would be 9 spots.
I would strongly encourage using the layout engine for such things, but you could simulate yourself with something like the following, which I haven't tested...
// given a prefix, like #"5/50" and a suffix like #"(80%)", return a string where they are combined
// add leading spaces so that the prefix is right-justified to a particular pixel position
//
- (NSString *)paddedPrefix:(NSString *)prefix andSuffix:(NSString *)suffix forLabel:(UILabel *)label {
// or get maxWidth some other way, depends on your app
CGFloat maxWidth = [self widthOfString:#"88888/50" presentedIn:label];
NSMutableString *mutablePrefix = [prefix mutableCopy];
CGFloat width = [self widthOfString:mutablePrefix presentedIn:label];
while (width<maxWidth) {
[mutablePrefix insertString:#" " atIndex:0];
}
// the number of blanks between the prefix and suffix is also up to you here:
return [NSString stringWithFormat:#"%# %#", mutablePrefix, suffix];
}
// answer the width of the passed string assuming an infinitely wide label (no wrapping)
//
- (CGFloat)widthOfString:(NSString *)string presentedIn:(UILabel *)label {
NSAttributedString *as = [[NSAttributedString alloc] initWithString:string attributes:#{NSFontAttributeName:label.font}];
CGRect rect = [as boundingRectWithSize:(CGSize){CGFLOAT_MAX, CGFLOAT_MAX}
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
return rect.size.width;
}

Calculating font size for a CCLabelTTF

Does anyone know a method to calculate the maximum font size for a CCLabelTTf of a specific CGSize? I have seen ways of calculating a font size for a specific width but not width and height. This calculates the font size for width:
-(int) getSizeForString:(NSString*)aString InWidth:(int)width
{
int startSize = 24;
while (startSize > 5) {
CGSize aSize = [aString sizeWithFont:[UIFont fontWithName:#"Verdana-Bold" size:startSize]];
if (aSize.width <= width) return startSize;
startSize--;
}
return 5;
}
If you know how to calculate size of text with a specific font, you can in the same way check height...
If you want to set string in more than one line you should use
[NSString sizeWithFont:font constrainedToSize:maxSize lineBreakMode:NSLineBreakByClipping];
This return you a CGSize for string with more than one lines if the text don't fit to maxSize

How To Get UIView Height Only?

I have this code :
_width.text = NSStringFromCGRect(_screen.frame);
and it gives me on my iPod 5:
{{0,20}, {320,548}}
how to get only height value = 548 so that I can use it to calculate another components.
thank you.
I guess you want a CGFloat, not a NSString if you want to calculate other components.
CGFloat height = _screen.frame.size.height;
Or if you want a string after all:
_width.text = [NSString stringWithFormat:#"%f", _screen.frame.size.height];
The frame property of UIView is a CGRect struct. To learn about CGRect go take a look at the documentation.
I would encourage you to use CGGeometry check the documentation, in your case use CGRectGetHeight().
CGFloat height = CGRectGetHeight(_screen.frame);
Try to use like this...
You can get Height Like this...
CGFloat height = _screen.frame.size.height;
_width.text = [NSString stringWithFormat:#"%f",height];
You can get Width Like this...
CGFloat width = _screen.frame.size.width;
_width.text = [NSString stringWithFormat:#"%f",width];
You can get x coordinate Like this Like this...
CGFloat x = _screen.frame.origin.x;
You can get y coordinate Like this Like this...
CGFloat y = _screen.frame.origin.y;
_screen.frame is a struct with a CGSize and a CGPoint.
CGPoint origin = _screen.frame.origin;
CGSize size = _screen.frame.size;
The struct CGPoint has the members x and y
CGFloat x = origin.x; // float on 32bit and double on 64bit
CGFloat y = origin.y;
The struct CGSize has the members width and height
CGFloat width = size.width;
CGFloat height = size.height;
You can print a message with a formatted NSString or C-String with the format specifier %
NSString *output = [NSString stringWithFormat:#"My screen height is: %f", height];
If you want to show the height as 543 not as 543.0, then use the float string specifier in the NSString creation method
NSString *string = [NSString stringWithFormat:#"Integer height is: %.0f", height];
// even with values like 0.9999 the output would be "Integer height is: 0"
If the property _width.text is a NSString.
_width.text = output;

With what should I replace the deprecated sizeWithFont: method?

I have a method that gives me the perfect size for a UITextView given a length of string (with the corresponding correct font size) :
- (NSInteger) heightOfLabel:(NSString*) string {
CGSize maximumLabelSize = CGSizeMake([[UIScreen mainScreen] bounds].size.width - 40, FLT_MAX);
CGSize expectedLabelSize = [[NSString stringTrimmedForLeadingAndTrailingWhiteSpacesFromString:string]
sizeWithFont:[UIFont systemFontOfSize:15]
constrainedToSize:maximumLabelSize
lineBreakMode:NSLineBreakByWordWrapping];
return expectedLabelSize.height + 5;
}
In fact, it still gives me a perfect fit, even in iOS7. Although now it comes up with a warning method that says I shouldn't use 'sizeWithFont:contrainedToSize:lineBreakMode'.
It now says I should be using -boundingRectWithSize:options:attributes:context:
This method isn't new to iOS7 and therefore i figure that it is okay to ask it on stack overflow, rather than going across to the official apple developers forum.
I have three questions:
1) Because it is deprecated, does that mean I should definitely replace it, despite it still working?
2) I have tried many different boundingRectWithSize: methods, with various variables but it is never perfect, it always seems to be slightly out (as many stackoverflow questions point out) - is there a perfect replacement with this none-deprecated method that does exactly the same as my previous method with as minimal hassle?
3) why remove this method? Is it because of the overlap with this other method?
After an hour of trial error I managed to make it work:
CGSize maximumLabelSize = CGSizeMake(tableView.width, MAXFLOAT);
NSStringDrawingOptions options = NSStringDrawingTruncatesLastVisibleLine |
NSStringDrawingUsesLineFragmentOrigin;
NSDictionary *attr = #{NSFontAttributeName: [UIFont systemFontOfSize:15]};
CGRect labelBounds = [string boundingRectWithSize:maximumLabelSize
options:options
attributes:attr
context:nil];
Update:
As Mr. T mentions in answer below : In iOS 7 and later, this method returns fractional sizes (in the size component of the returned CGRect); to use a returned size to size views, you must use raise its value to the nearest higher integer using the ceil function. ceilf function is recommended to use.
CGFloat height = ceilf(labelBounds.size.height);
I believe the function was deprecated because that series of NSString+UIKit functions were based on the UIStringDrawing library, which wasn't thread safe. If you tried to run them not on the main thread (like any other UIKit functionality), you'll get unpredictable behaviors. In particular, if you ran the function on multiple threads simultaneously, it'll probably crash your app. This is why in iOS 6, they introduced a the boundingRectWithSize:... method for NSAttributedStrings. This was built on top of the NSStringDrawing libraries and is thread safe.
If you look at the new NSString boundingRectWithSize:... function, it asks for an attributes array in the same manner as a NSAttributeString. If I had to guess, this new NSString function in iOS 7 is merely a wrapper for the NSAttributeString function from iOS 6.
On that note, if you were only supporting iOS 6 and iOS 7, then I would definitely change all of your NSString's sizeWithFont:... to the NSAttributeString's boundingRectWithSize. It'll save you a lot of headache if you happen to have a weird multi-threading corner case! Here's how I converted NSString's sizeWithFont:constrainedToSize::
What used to be:
NSString *text = ...;
CGFloat width = ...;
UIFont *font = ...;
CGSize size = [text sizeWithFont:font
constrainedToSize:CGSizeMake(width, CGFLOAT_MAX)];
Can be replaced with:
NSString *text = ...;
CGFloat width = ...;
UIFont *font = ...;
NSAttributedString *attributedText =
[[NSAttributedString alloc]
initWithString:text
attributes:#
{
NSFontAttributeName: font
}];
CGRect rect = [attributedText boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
CGSize size = rect.size;
Please note the documentation mentions:
In iOS 7 and later, this method returns fractional sizes (in the size
component of the returned CGRect); to use a returned size to size
views, you must use raise its value to the nearest higher integer
using the ceil function.
So to pull out the calculated height or width to be used for sizing views, I would use:
CGFloat height = ceilf(size.height);
CGFloat width = ceilf(size.width);
For linebreak issue:
- (CGFloat)heightNeededForText:(NSString *)text withFont:(UIFont *)font width:(CGFloat)width lineBreakMode:(NSLineBreakMode)lineBreakMode {
NSMutableParagraphStyle * paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.lineBreakMode = lineBreakMode;
CGSize size = [text boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX)
options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
attributes:#{ NSFontAttributeName: font, NSParagraphStyleAttributeName: paragraphStyle }
context:nil].size;
return ceilf(size.height);
}
Swift version of the Alexander of Norway's answer...
func heightNeededForText(text: NSString, withFont font: UIFont, width: CGFloat, lineBreakMode:NSLineBreakMode) -> CGFloat {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = lineBreakMode
let size: CGSize = text.boundingRectWithSize(CGSizeMake(width, CGFloat.max), options: [.UsesLineFragmentOrigin, .UsesFontLeading], attributes: [ NSFontAttributeName: font, NSParagraphStyleAttributeName: paragraphStyle], context: nil).size//text boundingRectWithSize:CGSizeMake(width, CGFLOAT_MA
return ceil(size.height);
}
In the code where you want to get the height just call the method like below...
let size = self.heightNeededForText(text as NSString, withFont: UIFont.systemFontOfSize(15.0), width: scrollView.frame.size.width - 20, lineBreakMode: NSLineBreakMode.ByWordWrapping) //Can edit the font size and LinebreakMode

NSTextAttachmentCell is a mile high

I'm editing a subset of HTML in an NSTextView[1] and I want to simulate an <hr> tag.
I've figured out that the way to do it is with NSTextAttachment and a custom NSTextAttachmentCell, and have the code all written to insert the attachment and cell. The problem is, there's an enormous amount of blank space below the cell.
This space is not part of the cell itself—if I paint the entire area of the cell red, it's exactly the right size, but the text view is putting the next line of text very far below the red. The amount seems to depend on how much text is above the cell; unfortunately, I'm working with long documents where <hr> tags are crucial, and this causes major problems with the app.
What the heck is going on?
The money parts of my cell subclass:
- (NSRect)cellFrameForTextContainer:(NSTextContainer *)textContainer
proposedLineFragment:(NSRect)lineFrag glyphPosition:(NSPoint)position
characterIndex:(NSUInteger)charIndex {
lineFrag.size.width = textContainer.containerSize.width;
lineFrag.size.height = topMargin + TsStyleBaseFontSize *
heightFontSizeMultiplier + bottomMargin;
return lineFrag;
}
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
characterIndex:(NSUInteger)charIndex
layoutManager:(NSLayoutManager *)layoutManager {
NSRect frame = cellFrame;
frame.size.height -= bottomMargin;
frame.size.height -= topMargin;
frame.origin.y += topMargin;
frame.size.width *= widthPercentage;
frame.origin.x += (cellFrame.size.width - frame.size.width)/2;
[color set];
NSRectFill(frame);
}
[1] I tried a WebView with isEditable set and the markup it produced was unusably dirty—in particular, I couldn't find a way to wrap text nicely in <p> tags.
To answer Rob Keniger's request for the code that inserts the horizontal rule attachment:
- (void)insertHorizontalRule:(id)sender {
NSAttributedString * rule = [TsPage newHorizontalRuleAttributedStringWithStylebook:self.book.stylebook];
NSUInteger loc = self.textView.rangeForUserTextChange.location;
if(loc == NSNotFound) {
NSBeep();
return;
}
if(loc > 0 && [self.textView.textStorage.string characterAtIndex:loc - 1] != '\n') {
NSMutableAttributedString * workspace = rule.mutableCopy;
[workspace.mutableString insertString:#"\n" atIndex:0];
rule = workspace;
}
if([self.textView shouldChangeTextInRange:self.textView.rangeForUserTextChange replacementString:rule.string]) {
[self.textView.textStorage beginEditing];
[self.textView.textStorage replaceCharactersInRange:self.textView.rangeForUserTextChange withAttributedString:rule];
[self.textView.textStorage endEditing];
[self.textView didChangeText];
}
[self.textView scrollRangeToVisible:self.textView.rangeForUserTextChange];
[self reloadPreview:sender];
}
And the method in TsPage that constructs the attachment string:
+ (NSAttributedString *)newHorizontalRuleAttributedStringWithStylebook:(TsStylebook*)stylebook {
TsHorizontalRuleCell * cell = [[TsHorizontalRuleCell alloc] initTextCell:#"—"];
cell.widthPercentage = 0.33;
cell.heightFontSizeMultiplier = 0.25;
cell.topMargin = 12.0;
cell.bottomMargin = 12.0;
cell.color = [NSColor blackColor];
NSTextAttachment * attachment = [[NSTextAttachment alloc] initWithFileWrapper:nil];
attachment.attachmentCell = cell;
cell.attachment = attachment;
NSAttributedString * attachmentString = [NSAttributedString attributedStringWithAttachment:attachment];
NSMutableAttributedString * str = [[NSMutableAttributedString alloc] initWithString:#""];
[str appendAttributedString:attachmentString];
[str.mutableString appendString:#"\n"];
return str;
}
Try changing your cell class's cellFrameForTextContainer:proposedLineFragment:glyphPosition: method to return NSZeroPoint for the origin of the cell's frame.
(I have no idea why this should work, but the questioner and I have been live-debugging it, and it actually does.)
Added by questioner: It looks like, even though the rect being returned is described as a "frame", its origin is relative to the line it's in, not to the top of the document. Thus, the return value's origin.y value should be set to zero if you want it on the same line.
(The origin.x value, on the other hand, does refer to the cell's position in the line, so it should be the same as lineFrag.origin.x unless you want to change the cell's horizontal location.)