Padding Spaces in a NSString - objective-c

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;
}

Related

Efficiently determine how much text can fit in a UILabel in IOS

I have an NSString and I want to know how much of that string will fit into a UILabel.
My code builds a test string by adding one character at a time from my original string. Each time I add a character I test the new string to see if it will fit into my label:
CGRect cutTextRect = [cutText boundingRectWithSize:maximumLabelSize options:NSStringDrawingUsesLineFragmentOrigin attributes:stringAttributes context:nil];
Then I compare the height of that rect to the height of my label to see if the string overflowed.
This works, but instruments shows me that the loop is taking up all my cpu time.
Can anyone think of or know of a faster way to do this?
Thanks!
While not the prettiest:
- (NSString *)stringForWidth:(CGFloat)width fullString:(NSString *)fullString
{
NSDictionary *attributes = #{NSFontAttributeName: label.font};
if ([fullString sizeWithAttributes:attributes].width <= width)
{
return fullString;
}
// Might be worth researching more regarding 'average' char size
CGFloat approxCharWidth = [#"N" sizeWithAttributes:attributes].width;
NSInteger approxNumOfChars = (NSInteger)(width / approxCharWidth);
NSMutableString *resultingString = [NSMutableString stringWithString:[fullString substringToIndex:approxNumOfChars]];
CGFloat currentWidth = [resultingString sizeWithAttributes:attributes].width;
if (currentWidth < width)
{
// Try to 'sqeeze' another char.
while (currentWidth < width && approxNumOfChars < fullString.length)
{
approxNumOfChars++;
[resultingString appendString:[fullString substringWithRange:NSMakeRange(approxNumOfChars - 1, 1)]];
currentWidth = [resultingString sizeWithAttributes:attributes].width;
}
}
// String might be oversized
if (currentWidth > width)
{
while (currentWidth > width)
{
[resultingString deleteCharactersInRange:NSMakeRange(resultingString.length - 1, 1)];
currentWidth = [resultingString sizeWithAttributes:attributes].width;
}
}
// If dealing with UILabel, it's safer to have a smaller string than 'equal',
// 'clipping wise'. Otherwise, just use '<=' or '>=' instead of '<' or '>'
return [NSString stringWithString:resultingString];
}
There are a couple of loops, but each one is a 'fine tuning' and should only run a small number of times.
One way to improve efficiency is to get a better starting point than calculating be how many N's can fit in a given width.
I'm open to suggestions about that.
-Edit:
Regarding multiline label, once I know a given text for width, I can expect the following text (if any) will go to the next line.
In other words, getting 'text for width' is the tricky part, 'width for text' we get for free.

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

RTL & LTR (bidirectional) in UITextView

I'm trying to save the content of a UITextView which contains lines of text formatted both RTL and LTR.
The problem is that UITextView checks only the first character to format direction. Let's assume I'm in "edit" mode and write this text (__ means spaces):
text1_______________________________________
____________________________________________אקסא
text2_______________________________________
and after saving we lost RTL for אקסא. Now I'd like to edit this text once again which now looks like:
text1_______________________________________
אקסא
text2_______________________________________
I'm not able to mix \u200F with \u200E directional characters in one UITextView.
How to manage this and save correctly bidirectional text from UITextView?
Here is a quick proof of concept using NSAttributedString :
- Split the text in paragraphs
- For each paragraph, detect the main language
- Create an attributed text with the correct alignmenent for the corresponding range
// In a subclass of `UITextView`
+ (UITextAlignment)alignmentForString:(NSString *)astring {
NSArray *rightToLeftLanguages = #[#"ar",#"fa",#"he",#"ur",#"ps",#"sd",#"arc",#"bcc",#"bqi",#"ckb",#"dv",#"glk",#"ku",#"pnb",#"mzn"];
NSString *lang = CFBridgingRelease(CFStringTokenizerCopyBestStringLanguage((CFStringRef)astring,CFRangeMake(0,[astring length])));
if (astring.length) {
if ([rightToLeftLanguages containsObject:lang]) {
return NSTextAlignmentRight;
}
}
return NSTextAlignmentLeft;
}
- (void)setText:(NSString *)str { // Override
[super setText:str];
// Split in paragraph
NSArray *paragraphs = [self.text componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
// Attributed string for the whole string
NSMutableAttributedString *attribString = [[NSMutableAttributedString alloc]initWithString:self.text];
NSUInteger loc = 0;
for(NSString *paragraph in paragraphs) {
// Find the correct alignment for this paragraph
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc]init];
[paragraphStyle setAlignment:[WGTextView alignmentForString:paragraph]];
// Find its corresponding range in the string
NSRange range = NSMakeRange(loc, [paragraph length]);
// Add it to the attributed string
[attribString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range];
loc += [paragraph length];
}
[super setAttributedText:attribString];
}
Also, I recommend reading the Unicode BiDi Algorithm to manage more complex use cases.

Store a NSColor as a string

I currently have a Core Data database that stores data and I wish to also store an NSColor into it but It does not accept NSColor as an object. My solution would be to store it as a string in the database and have it read into a NSColor when loaded. How would I do this?
For example, If I had a colour like [NSColor redColor] how would I store it in a database (as a string) and then retrieve it. This is a basic example and it would be more complicated RGB colors in the end.
Thanks.
You should consider using NSData as container for storing unsupported data types in Core Data. To access NSColor as NSData you will need to mark attribute as transformable and create reversible NSValueTransformer class to transform NSColor as NSData.
Useful Link: Non-Standard Persistent Attributes
I agree with the answers that recommend using NSData for storing colors in a Core Data store. That said, there may be times when it might be useful to store a color in a string, and it's certainly not difficult to do. I'd suggest creating a category on NSColor:
#interface NSColor (NSString)
- (NSString*)stringRepresentation;
+ (NSColor*)colorFromString:(NSString*)string forColorSpace:(NSColorSpace*)colorSpace;
#end
#implementation NSColor (NSString)
- (NSString*)stringRepresentation
{
CGFloat components[10];
[self getComponents:components];
NSMutableString *string = [NSMutableString string];
for (int i = 0; i < [self numberOfComponents]; i++) {
[string appendFormat:#"%f ", components[i]];
}
[string deleteCharactersInRange:NSMakeRange([string length]-1, 1)]; // trim the trailing space
return string;
}
+ (NSColor*)colorFromString:(NSString*)string forColorSpace:(NSColorSpace*)colorSpace
{
CGFloat components[10]; // doubt any color spaces need more than 10 components
NSArray *componentStrings = [string componentsSeparatedByString:#" "];
int count = [componentStrings count];
NSColor *color = nil;
if (count <= 10) {
for (int i = 0; i < count; i++) {
components[i] = [[componentStrings objectAtIndex:i] floatValue];
}
color = [NSColor colorWithColorSpace:colorSpace components:components count:count];
}
return color;
}
#end
I've checked that the code above compiles and works about as advertised. A small sample program produces appropriate output:
int main(int argc, const char * argv[])
{
#autoreleasepool {
NSLog(#"Red is: %#", [[NSColor redColor] stringRepresentation]);
NSLog(#"Cyan is: %#", [[NSColor cyanColor] stringRepresentation]);
NSLog(#"Read in: %#", [NSColor colorFromString:[[NSColor redColor] stringRepresentation]
forColorSpace:[NSColorSpace deviceRGBColorSpace]]);
}
return 0;
}
Output:
Red is: 1.000000 0.000000 0.000000 1.000000
Cyan is: 0.000000 1.000000 1.000000 1.000000
Read in: NSCustomColorSpace Generic RGB colorspace 1 0 0 1
It might make sense to store the color space in the string so you don't have to specify it when you go from string to color. Then again, if you're just going to store these strings and read them again, you should be using NSData anyway. Using strings makes more sense if you need to write colors into some sort of human-readable file, or perhaps as a debugging aid.
NSColor supports the NSCoding protocol, so you can use the -encodeWithCoder: method to save it to an archive, and you can use -initWithCoder: to load it from an archive.
Property lists do not store colors and Apple recommends you store them as NSData not as NSString, you should probably do the same. See Apple's instructions here.
Here are simple functions for converting an NSColor to and from an NSString. This example assumes we're using an RGB color space, but
it can be easily adapted for others. For example, NSStringFromColor() could include the color space in the string and use that information when converting back to a color in NSColorFromString().
Usage:
NSString *theColorString = NSStringFromColor(theColor);
NSColor *theColor = NSColorFromString(theColorString);
The functions:
NSString *NSStringFromColor(NSColor *theColor)
{
CGFloat red, green, blue, alpha;
[theColor getRed:&red green:&green blue:&blue alpha:&alpha]; // assumes RGB color space
NSString *theColorString = [NSString stringWithFormat:#"%f %f %f %f",red,green,blue,alpha];
return theColorString;
}
NSColor *NSColorFromString(NSString *theColorString)
{
if ( theColorString.length == 0 ) {
theColorString = #"0.9 0.9 0.95 1.0"; // a default color
}
NSArray <NSString *> *theColors = [theColorString componentsSeparatedByString:#" "];
if ( theColors.count == 4 ) { // sanity
// unpack the color
NSColor *theColor = [NSColor colorWithSRGBRed:theColors[0].floatValue
green:theColors[1].floatValue
blue:theColors[2].floatValue
alpha:theColors[3].floatValue];
return theColor;
}
return nil; // theColorString format error
}

Truncate a string

I have a NSTableView that shows the path of files in one column. When the user resizes the tableview I want the pathname (e.g. /Users/name/testfile.m) to be resized, but I want the end of the pathname (e.g. ...name/testfile.m) to be visible and not the start (e.g. /Users/test/te...) of the path as happens by default. I wrote a function that successfully does what I want to do, but the tableview flickers while redrawing as the user scales the tableview. I think there must be a better, more elegant algorithm for doing this, but I have looked into the documentation for NSString and on Stackoverflow and I cant find anything that gives a better solution. If anyone has a more elegant solution to this problem that would be greatly appreciated. Thanks! Cheers, Trond
My current function:
-(NSString *) truncateString:(NSString *) myString withFontSize:(int) myFontSize withMaxWidth:(NSInteger) maxWidth
{
// Get the width of the current string for a given font
NSFont *font = [NSFont systemFontOfSize:myFontSize];
CGSize textSize = NSSizeToCGSize([myString sizeWithAttributes:[NSDictionary dictionaryWithObject:font forKey: NSFontAttributeName]]);
NSInteger lenURL =(int)textSize.width;
// Prepare for new truncated string
NSString *myStringShort;
NSMutableString *truncatedString = [[myString mutableCopy] autorelease];
// If the available width is smaller than the string, start truncating from first character
if (lenURL > maxWidth)
{
// Get range for first character in string
NSRange range = {0, 1};
while ([truncatedString sizeWithAttributes:[NSDictionary dictionaryWithObject:font forKey: NSFontAttributeName]].width > MAX(TKstringPad,maxWidth))
{
// Delete character at start of string
[truncatedString deleteCharactersInRange:range];
}
myStringShort = [NSString stringWithFormat:#"...%#",truncatedString];
}
else
{
myStringShort=myString;
}
return myStringShort;
}
The typical approach would be simply:
[tableViewCell setLineBreakMode:NSLineBreakByTruncatingHead];
As Dondragmer noted, this property may also be set in Xcode's NIB editor.