I have a UILabel whose size is calculated with sizeWithFont:
method. The line break mode is set to UILineBreakModeWordWrap
(same flag is used when calculating the size with sizeWithFont:
)...
Everything works great, label is properly sized and displays my text as required.
Now I need to know the lines that are used to display the label (or the lines that are generated when sizeWithFont:
is used). I could technically write my own implementation of line breaking based on spaces/caret returns, but then it's not going to be guaranteed the same way as Apple's implementation and hence the resulting lines will not be the ones that are used to calculate the size of text, nevermind the fact of reinventing the wheel.
Ideally, I would pass my string, specify the width and line break mode and receive an array of strings representing the visual lines of text.
Any ideas how to make this happen in the most elegant way?
For example, to make UILabel support two lines, we will set the numberOfLines property to 2. Although, in some situations, setting the numberOfLines property alone is not enough. For example, if the height of UILabel is set, then it will have a higher priority and the number of lines property will not be respected.
To calculate the number of lines that a UILabel
has after wrapping it's text you will need to find the leading (line height) of your UILabel
's font (label.font.leading
) and then divide the height of your multi-line UILabel
by the height of each line to yield the number of lines.
Here's an example:
- (void)viewDidLoad {
[super viewDidLoad];
UILabel *label = [[[UILabel alloc] initWithFrame:CGRectZero] autorelease];
label.numberOfLines = 0;
label.lineBreakMode = UILineBreakModeWordWrap;
label.text = @"Some really really long string that will cause the label's text to wrap and wrap and wrap around. Some really really long string that will cause the label's text to wrap and wrap and wrap around.";
CGRect frame = label.frame;
frame.size.width = 150.0f;
frame.size = [label sizeThatFits:frame.size];
label.frame = frame;
CGFloat lineHeight = label.font.leading;
NSUInteger linesInLabel = floor(frame.size.height/lineHeight);
NSLog(@"Number of lines in label: %i", linesInLabel);
[self.view addSubview:label];
}
Or, you could do it in two lines:
[label sizeToFit];
int numLines = (int)(label.frame.size.height/label.font.leading);
I don't think there is any silver bullet for this.
Here is a category method that seems to work for the few basic test cases I threw at it. No guarantees it won't break with something complex!
The way it works is to move through the string testing to see if a range of words fits in the width of the label. When it calculates that the current range is too wide it records the last-fitting range as a line.
I don't claim this is efficient. A better way may just to be to implement your own UILabel...
@interface UILabel (Extensions)
- (NSArray*) lines;
@end
@implementation UILabel (Extensions)
- (NSArray*) lines
{
if ( self.lineBreakMode != UILineBreakModeWordWrap )
{
return nil;
}
NSMutableArray* lines = [NSMutableArray arrayWithCapacity:10];
NSCharacterSet* wordSeparators = [NSCharacterSet whitespaceAndNewlineCharacterSet];
NSString* currentLine = self.text;
int textLength = [self.text length];
NSRange rCurrentLine = NSMakeRange(0, textLength);
NSRange rWhitespace = NSMakeRange(0,0);
NSRange rRemainingText = NSMakeRange(0, textLength);
BOOL done = NO;
while ( !done )
{
// determine the next whitespace word separator position
rWhitespace.location = rWhitespace.location + rWhitespace.length;
rWhitespace.length = textLength - rWhitespace.location;
rWhitespace = [self.text rangeOfCharacterFromSet: wordSeparators options: NSCaseInsensitiveSearch range: rWhitespace];
if ( rWhitespace.location == NSNotFound )
{
rWhitespace.location = textLength;
done = YES;
}
NSRange rTest = NSMakeRange(rRemainingText.location, rWhitespace.location-rRemainingText.location);
NSString* textTest = [self.text substringWithRange: rTest];
CGSize sizeTest = [textTest sizeWithFont: self.font forWidth: 1024.0 lineBreakMode: UILineBreakModeWordWrap];
if ( sizeTest.width > self.bounds.size.width )
{
[lines addObject: [currentLine stringByTrimmingCharactersInSet:wordSeparators]];
rRemainingText.location = rCurrentLine.location + rCurrentLine.length;
rRemainingText.length = textLength-rRemainingText.location;
continue;
}
rCurrentLine = rTest;
currentLine = textTest;
}
[lines addObject: [currentLine stringByTrimmingCharactersInSet:wordSeparators]];
return lines;
}
@end
use like this:
NSArray* lines = [_theLabel lines];
int count = [lines count];
Just call below method and pass either UILabel
or UITextView
:
-(NSInteger)getNumberOfLinesInLabelOrTextView:(id)obj
{
NSInteger lineCount = 0;
if([obj isKindOfClass:[UILabel class]])
{
UILabel *label = (UILabel *)obj;
// This method is deprecated in iOS 7.0 or later
// CGSize requiredSize = [label.text sizeWithFont:label.font constrainedToSize:label.frame.size lineBreakMode:label.lineBreakMode];
CGSize requiredSize = [label.text boundingRectWithSize:CGSizeMake(CGRectGetWidth(label.frame), CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:label.font} context:nil].size;
int charSize = label.font.leading;
int rHeight = requiredSize.height;
lineCount = rHeight/charSize;
}
else if ([obj isKindOfClass:[UITextView class]])
{
UITextView *textView = (UITextView *)obj;
lineCount = textView.contentSize.height / textView.font.leading;
}
return lineCount;
}
Now call this method:-
NSLog(@"%d",[self getNumberOfLinesInLabelOrTextView:label]);
NSLog(@"%d",[self getNumberOfLinesInLabelOrTextView:textView]);
UPDATED: SWIFT CODE
func getNumberOfLinesInLabelOrTextView(obj:AnyObject) -> NSInteger {
var lineCount: NSInteger = 0
if (obj.isKindOfClass(UILabel)) {
let label: UILabel = obj as! UILabel
let requiredSize: CGSize = (label.text)!.boundingRectWithSize(CGSizeMake(CGRectGetWidth(label.frame), CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName: label.font], context: nil).size
let charSize: CGFloat = label.font.leading
let rHeight: CGFloat = requiredSize.height
lineCount = (NSInteger)(rHeight/charSize)
}
else if (obj.isKindOfClass(UITextView)){
let textView: UITextView = obj as! UITextView
lineCount = (NSInteger)(textView.contentSize.height / textView.font.leading)
}
return lineCount
}
Now call this method:-
println("%d \(self.getNumberOfLinesInLabelOrTextView(textView))")
println("%d \(self.getNumberOfLinesInLabelOrTextView(label))")
Note: leading
- use lineHeight. does not return actual leading. will be formally deprecated in future.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With