Search code examples
iosobjective-csprite-kitnsstringnsarray

Fitting multi-line text into a dynamically size-changing node


A multiline auto typing text box class (which uses an SKNode as the parent) is created using basically 2 elements:

  • an SKSpriteNode that acts as text box frame & background image/texture holder.
  • an NSMutableArray containing a set limited amount (rows) of NSStrings that each have a set character length.

After modifying this text box class so that it can be initialized with any frame width & height, I realized I didn't program the NSMutableArray to automatically change its content in a such way that it nicely fits within the background node (with a bit of padding involved as well). So here I am wondering how to do that since NSString's can only return the character count and not the width & height of each string in points (points could have maybe helped me create character constraints in some way).

Right now, the NSMutableArray uses a hardcoded maximum character count per NSString & a maximum row count for the entire array (it's 5 rows right now and when that limit is reached, a new "page"/array is created). This forces me to manually re-adjust these parameters every time I change the background node frame size which defeats the purpose of the class allowing the background frame to change.

Thing is, I'm trying to solve this in such a way that when I post this class on github, I want the solution to take into consideration any fontName & fontSize.

What are my options for solving this problem?


Solution

  • I've done something similar to this. It doesn't work 100% as to what you want, but should be similar enough. It uses a root node and from there, it will build multi-line text using an array of NSString which will in turn be used to build the SKLabelNode.

    I'll outline what I did. I should also say I only run this when new text is set. In other words, I do not incur the penalty of deriving the information every frame. Only once.

    The generalized steps are:

    • You will iterate over each character in the text string. Note I do this because my code supports word wrapping as well as other alignment capabilities. So for me, I want that level of control. As this is being done only upon creation, I'm fine with the overhead. If you don't want to word wrap you could always just create an array of words and work from there.
    • As you iterate over each character, you'll be generating an array of lines. Where each line in the array is a line that will fit in your frame. For now let's not worry about vertical constraints. So here we are primarily worried about width. For the current line, each character you are iterating over will get added to the current line. For this potential line string, you will use NSString's sizeWithAttributes, which is configured for your font. For example in my code it is an NSDictionary which contains: NSFontAttributeName : [UIFont fontWithName:self.fontName size:self.size]. This will be used to check the width, if that width exceeds the frame width, you are overrunning the line.

    So the code may look something like:

            size = [line sizeWithAttributes:attributes];
    
            if (size.width > maxTextWidth) {
                needNewline = YES;
            }
    
    • If you have overrun a line, you need to determine if you are word wrapping. If you are, you can just add the current line (minus one character) to the lines array. If not you have prune off the last word in the current line and then add that to the array of lines.
    • The tricky parts are dealing with whitespace and handling non-word wrapped overflow. I have not addressed whitespace but you need to consider this very much in your code. Additionally, you also do want to factor in leading pixels, etc.

    Once you have your array of lines, you can then create your children SKLabelNodes. I add them to the root, which allows me to move the group anywhere it needs to be.

    The real key here is the lines array generation.