I need a method like CTFrameGetVisibleStringRange that can give me the text that will be rendered in a given size supplied with a line break mode (i.e. word wrap). For example I have a long line of text.. and I have a given rectangle to draw the text wrapped in it, but wherever the text gets trunecated, I continue rendering it in another area where it left off. So I need a method like:
NSString * text = "The lazy fox jumped over the creek";
[text drawAtPoint:CGPointMake(0, 0) forWidth:20 withFont:[UIFont fontWithName:@"Arial" size:10] lineBreakMode:UILineBreakModeWordWrap];
// now I do I know how much it drew before it stopped rendering?
Anyone have any ideas?
**EDITED: Please see my solution.
I had a similar problem, and I used the solution Mike posted.
It turned out, however, that trimToWord
was often giving me a few too many words than could fit on my UILabel size specified. I found out that if I changed the while loop operator to a >= and not just a >, it worked perfectly.
I also added a few ivars(chopIndex
and remainingBody
) that I used to get the remaining string so I could display it in my next UILabel.
Here's the solution I used.
-(NSString*) rewindOneWord:(NSString*) str{
// rewind by one word
NSRange lastspace = [str rangeOfString:@" " options:NSBackwardsSearch];
if (lastspace.location != NSNotFound){
int amount = [str length]-lastspace.location;
chopIndex -= amount;
return [str substringToIndex:lastspace.location];
}else {
// no spaces, lets just rewind 2 characters at a time
chopIndex -= 2;
return [str substringToIndex:[str length]-2];
}
}
// returns only how much text it could render with the given stipulations
-(NSString*) trimToWord:(NSString*)str sizeConstraints:(CGSize)availableSize withFont:(UIFont*)font{
if(str == @"")
return str;
CGSize measured = [str sizeWithFont:font constrainedToSize:CGSizeMake(availableSize.width, CGFLOAT_MAX) lineBreakMode:UILineBreakModeWordWrap];
// 'guess' how much we will need to cut to save on processing time
float choppedPercent = (((double)availableSize.height)/((double)measured.height));
if(choppedPercent >= 1.0){
//entire string can fit in availableSize
remainingBody = @"";
return str;
}
chopIndex = choppedPercent*((double)[str length]);
str = [str substringToIndex:chopIndex];
// rewind to the beginning of the word in case we are in the middle of one
do{
str = [self rewindOneWord:str];
measured = [str sizeWithFont:font constrainedToSize:availableSize lineBreakMode:UILineBreakModeWordWrap];
}while(measured.height>=availableSize.height);
//increment past the last space in the chopIndex
chopIndex++;
//update the remaining string
remainingBody = [remainingBody substringFromIndex:chopIndex];
return str;
}
Here is a solution. It is fairly fast. It 'guesses' where to chop first and then rolls back word by word. sizewithFont calls are fairly expensive so this intial 'guess' step is important. The main method is trimToWord: sizeConstraints:withFont.
Feel free to comment on how I could improve this.
-(NSString*) rewindOneWord:(NSString*) str{
// rewind by one word
NSRange lastspace = [str rangeOfString:@" " options:NSBackwardsSearch];
if (lastspace.location != NSNotFound){
int amount = [str length]-lastspace.location;
return [str substringToIndex:lastspace.location];
}else {
// no spaces, lets just rewind 2 characters at a time
return [str substringToIndex:[str length]-2];
}
}
// returns only how much text it could render with the given stipulations
-(NSString*) trimToWord:(NSString*) str sizeConstraints:(CGSize) avail withFont:(UIFont*) font{
CGSize measured = [str sizeWithFont:font constrainedToSize:CGSizeMake(avail.width, 1000000) lineBreakMode:UILineBreakModeWordWrap];
// 'guess' how much we will need to cut to save on processing time
float choppedPercent = (((double)avail.height)/((double)measured.height));
if (choppedPercent >= 1.0){
return str;
}
int chopIndex = choppedPercent*((double)[str length]);
str = [str substringToIndex:chopIndex];
// rewind to the beginning of the word in case we are in the middle of one
str = [self rewindOneWord:str];
measured = [str sizeWithFont:font constrainedToSize:avail lineBreakMode:UILineBreakModeWordWrap];
while (measured.height>avail.height){
str = [self rewindOneWord:str];
measured = [str sizeWithFont:font constrainedToSize:avail lineBreakMode:UILineBreakModeWordWrap];
}
return str;
}
I do'nt think,there is subsitute for CTFrameGetVisibleStringRange
, Although we can get the same with the use of the below method.
- (CGSize)sizeWithFont:(UIFont *)font forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode
Apple Documentation
http://developer.apple.com/library/ios/#documentation/uikit/reference/NSString_UIKit_Additions/Reference/Reference.html
EDITED: The below code show my approach
NSString * text = "The lazy fox jumped over the creek";
NSArray* m_Array = [text componentsSeparatedByCharactersInSet: [NSCharacterSet characterSetWithCharactersInString:@" "]];
CGSize mySize = CGSizeMake(300,180);
NSMutableString* myString = [[NSMutableString alloc] initWithString:@""];
//The below code till the end of the while statement could be put in separate function.
CGSize tempSize = CGSizeMake(0,0);
NSInteger index = 0 ;
do
{
[myString appendString:[m_Array objectAtIndex:index]];
tempSize = [myString sizeWithFont:myfont constrainedToSize:
CGSizeMake(mySize.width, CGFLOAT_MAX) lineBreakMode: UILineBreakModeWordWrap];
index++;
}while(tempSize.height < mySize.height && index <= [m_Array count])
//Remove the string items from m_Array till the (index-1) index,
[self RemoveItems:m_Array tillIndex:(index-1)];//Plz define you own
//you have the myString which could be fitted in CGSizeMake(300,180);
//Now start with remaining Array items with the same way as we have done above.
}