12

Possible Duplicate:
UITextView : get text with wrap info

I have been scouting the NSString library and numerous libraries for a function that can take a long string like this :

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui.

and together with a CGSize or float indicating the width, and the font being used, and return me a string with \n breaks and the words wrapped.

Result (roughly) :

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac\n
egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet.\n
 Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. \n
placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra.\n
 Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi.\n
 Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci,\n
 sagittis tempus lacus enim ac dui. 

I already know that UITextViews and such do this, but this is not helpful since I need to render the text in a raw OpenGL landscape, so I am using no regular UI elements.

I know this either exists as a framework, or a public class somewhere. I just simply can't find any unified way of handling this.

I imagine it is close to [NSString sizeWithFont:forWidth:lineBreakMode:], but i do not need the size, I need the string itself.

Community
  • 1
  • 1
Nils Munch
  • 8,805
  • 11
  • 51
  • 103
  • Guess you'll have to write up something on your own using your already mentioned sizeWithFont call. – jimpic Nov 27 '12 at 13:56
  • @cem, even though the question is not the same, the solution is close enough that I can make a function to feed my problem. – Nils Munch Nov 27 '12 at 14:13
  • What API do you use to draw the string? If you've rendered each character to a texture (which may break horribly for a lot of non-ASCII text), then just use the widths measured there; UIKit's string drawing uses "fractional" widths which is probably not what you want. Alternatively, use UIKit to draw to a bitmap and then move that into OpenGL (but note that glDrawPixels() can be slow in some cases). Also note that UIKit string drawing (and probably also `-sizeWithFont:...`) is **not** thread-safe in iOS 5.x: http://stackoverflow.com/questions/11589768/bug-in-uikit-string-drawing-method – tc. Nov 27 '12 at 14:19

2 Answers2

10

There really is no need to reinvent this wheel, since it is exactly what the text engine does for you every time you wrap text. And what is the text engine? It is Core Text. If you drop down to the level of Core Text and have a CTFramesetter lay out the text for you, you can learn where it is putting the line breaks by asking for the resulting CTLines.

The documentation will get you started:

http://developer.apple.com/library/ios/#documentation/StringsTextFonts/Conceptual/CoreText_Programming/Operations/Operations.html

And there are lots of good tutorials on the Web.

Simple example:

NSString* s = @"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do "
@"eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut "
@"enim ad minim veniam, quis nostrud exercitation ullamco laboris "
@"nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor "
@"in reprehenderit in voluptate velit esse cillum dolore eu fugiat "
@"nulla pariatur. Excepteur sint occaecat cupidatat non proident, "
@"sunt in culpa qui officia deserunt mollit.";
NSAttributedString* text = [[NSAttributedString alloc] initWithString:s];

CTFramesetterRef fs =
CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)text);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0,0,200,100000));
CTFrameRef f = CTFramesetterCreateFrame(fs, CFRangeMake(0, 0), path, NULL);
CTFrameDraw(f, NULL);

NSArray* lines = (__bridge NSArray*)CTFrameGetLines(f);
for (id aLine in lines) {
    CTLineRef theLine = (__bridge CTLineRef)aLine;
    CFRange range = CTLineGetStringRange(theLine);
    NSLog(@"%ld %ld", range.location, range.length);
}
CGPathRelease(path);
CFRelease(f);
CFRelease(fs);

As you will see, the output shows the range of each line of wrapped text. Isn't this the sort of thing you're after?

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 2
    Forgive me if this is a stupid question, since I haven't gotten a chance to look into it, but now that iOS 7 has NSLayoutManager and NSTextStorage, can we do the same thing you have done above without dealing directly with the CoreText framework? E.g. by using some of these higher level frameworks instead? – Senseful Oct 25 '13 at 21:48
  • 2
    It is not a stupid question at all, and yes we can! And it's much easier. The layout manager will happily tell you the complete glyphs/text of every line fragment. – matt Oct 25 '13 at 22:14
  • 2
    Awesome, thanks for the info. I posted it as a separate question: [How do I get word wrap information with the new iOS 7 APIs?](http://stackoverflow.com/questions/19600372/how-do-i-get-word-wrap-information-with-the-new-ios-7-apis) – Senseful Oct 25 '13 at 22:30
1

Seems like there is no factory way of doing this, so have started constructing a class function to handle this, based on the solution introduced at this closely related stack :

+ (NSString*)wrappedString:(NSString*)string withFont:(UIFont*)font andWidth:(float)width {
    NSMutableString *resultString = [[NSMutableString alloc] initWithString:@""];

    CGSize textSize = [string sizeWithFont:font];
    float textWidth = textSize.width;
    if (textWidth < width) {
        return string;
    }
    float wordLength;
    float lineLength;
    NSUInteger length = [string length];
    unichar buffer[length];
    [string getCharacters:buffer range:NSMakeRange(0, length)];

    NSString *singleLine = @"";
    NSString *word = @"";
    NSString *longWord = @"";

    for (NSUInteger i = 0; i < length; i++) {

        unichar character = buffer[i];
        if (character != '\n') {
            word = [NSString stringWithFormat:@"%@%c", word, character];
        }

        if (character == '\n') {
            float wordLength = [word sizeWithFont:font].width;
            float lineLength = [singleLine sizeWithFont:font].width;
            if ((lineLength + wordLength) > width) {
                [resultString appendString:singleLine];
                [resultString appendString:@"\n"];
                singleLine = @"";
                singleLine = [singleLine stringByAppendingFormat:@"%@\n",word];
                word = @"";
            } else {
                singleLine = [singleLine stringByAppendingString: word];
                word = @"";
                [resultString appendString:singleLine];
                [resultString appendString:@"\n"];
                singleLine = @"";
            }
        }

        else if (character == ' ') {
            float wordLength = [word sizeWithFont:font].width;
            float lineLength = [singleLine sizeWithFont:font].width;

            if ((lineLength + wordLength) > width) {
                if (wordLength > textWidth) {
                    [resultString appendString:singleLine];
                    [resultString appendString:@"\n"];
                    singleLine = @"";
                    int j = 0;
                    for (; j < [word length]; j++) {
                        unichar longChar = [word characterAtIndex:j];
                        longWord = [NSString stringWithFormat:@"%@%c", longWord, longChar];
                        float longwordLength = [longWord sizeWithFont:font].width;
                        float longlineLength = [singleLine sizeWithFont:font].width;
                        if ((longlineLength + longwordLength) >= width) {
                            singleLine = [singleLine stringByAppendingString:longWord];
                            word = @"";
                            longWord = @"";
                            break;
                        }
                    }

                }
                [resultString appendString:singleLine];
                [resultString appendString:@"\n"];
                singleLine = @"";
            }
            singleLine = [singleLine stringByAppendingString: word];
            word = @"";
        }
    }

    wordLength = [word sizeWithFont:font].width;
    lineLength = [singleLine sizeWithFont:font].width;

    if (wordLength > 0) {
        if ((lineLength + wordLength) > width) {
            [resultString appendString:singleLine];
            [resultString appendString:@"\n"];
            singleLine = @"";
        }
        singleLine = [singleLine stringByAppendingString:word];
    }


    if (lineLength > 0) {
        [resultString appendString:singleLine];
        [resultString appendString:@"\n"];
    }
    return resultString;
}
Community
  • 1
  • 1
Nils Munch
  • 8,805
  • 11
  • 51
  • 103