Computing text size with NSLayoutManager - ios

I'm working on a view which uses TextKit framework to typeset text in columns like this:
I use my UIView's bounds with edge insets (black rectangle) to compute 10 CGRects which I then transform into NSTextContainers (red rectangles). In drawRect: I pass those to the NSLayoutManager which typesets and draws the glyphs for me.
My question is: How can I compute the number of columns required? I can draw a constant number of columns but with varying text lengths, I need to adjust the number of columns programmatically.
I found the method [NSLayoutManager glyphRangeForTextContainer:] which returns range of length 0 when the text container is to be left empty. Therefore, I could loop to create text containers and use this method to determine if more containers are needed. However, this method is said to be inefficient as it triggers the layout computation and I'm not happy running it in a loop perhaps hundreds of times over.
There has to be a better way!
Thanks for your answers, Pete.

Well, after some digging through the TextKit framework I've finally found the answer.
My code works in a loop like this:
while ([self needsMoreColumns]) {
[self addColumn];
}
...
- (BOOL)needsMoreColumns {
// Always create at least one column
if (self.layoutManager.textContainers.count == 0)
return YES;
// Find out the glyph range of the last column
NSRange range = [self.layoutManager glyphRangeForTextContainer:[self.layoutManager.textContainers lastObject]];
NSUInteger glyphs = [self.layoutManager numberOfGlyphs];
// Compare it with the number of glyphs
return range.location + range.length < glyphs;
}
I didn't include the method [self addColumn] as it's a no brainer. It simply uses the geometry of my layout and position of the last column (if any) to compute the CGRect of the next one. Then, it creates NSTextContainer with respective size and stores the origin property of the rectangle in a dedicated array for drawing purposes.
I've also discovered methods [NSLayoutManager firstUnlaidCharacterIndex] and [NSLayoutManager firstUnlaidGlyphIndex] but they don't seem to work as expected. After laying out three columns worth of text in only one column, they returned the length of the entire string and not the position of the first character which didn't fit into the first column. That's why I rather used the range-based approach.
That's all folks, be safe!
Pete.

Related

Optimizing SKLabelNode to reduce performance issues

Creating an RPG with SpriteKit has me creating SKLabelNode's for displaying all game text. I'm suffering performance issues (FPS dropping & the game lagging) every time my multi-line text box begins auto typing inputted text which uses SKLabelNode's. I also see performance hits when "summoning" UI elements, such as character stat panels (with lots of SKLabelNode's for skill levels, skills names, etc.).
Veteran developers say pre-loading data always helps, thus I did the following (which has reduced the performance hit but hasn't eliminated it & the text box still lags):
// Preloaded model label (private property that is initialized in the class init method).
_modelLabel = [[SKLabelNode alloc] initWithFontNamed:_fontName];
_modelLabel.fontColor = _fontColor;
_modelLabel.fontSize = _fontSize;
_modelLabel.text = #"T";
// Constantly created label.
SKLabelNode *rowText = [[SKLabelNode alloc] initWithFontNamed:self.modelLabel.fontName];
rowText.fontColor = self.modelLabel.fontColor;
rowText.fontSize = self.modelLabel.fontSize;
rowText.text = row;
...
[self addChild:rowText];
I would be grateful for getting some more optimizations tricks when using SKLabelNode's.
Here are the tips I have collected thus far:
Preloading is only needed when using a font that is not available via iOS.
To get the font to actually load you need to also set the text for the preload to work.
Try to preload SKLabelNode with all chars you need. Maybe, SpriteKit renders only required glyphs into texture.
// Preloaded model label (private property that is initialized in the class init method).
_modelLabel = [[SKLabelNode alloc] initWithFontNamed:_fontName];
_modelLabel.fontColor = _fontColor;
_modelLabel.fontSize = _fontSize;
_modelLabel.text = #"ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghijklmnopqrstuvwxyz0123456789-+=_()";
And then add your model into scene with minimal alpha (but no zero, because if node has 0 alpha, it will not drawn); Try to keep that node in scene
_modelLabel.alpha = CGFLOAT_MIN;
[self addChild:_modelLabel];
If not helps:
Organize nodes by aligning their zPosition (aka z-index). You should disable ignoresSiblingOrder for having control over zPosition.
Make your all SKLabelNode be drawn at one z level.
Switch to bitmap fonts. Writing custom bitmap font renderer is super easy. The only thing you should remember - Make all glyphs be in only texture atlas, so you can draw your text in one draw call
Use native labels. I'm not sure, how much does it help. If you're having heavy FPS drops (~5..10FPS) This may help you
It seems to me you are creating new labels every time you need text. If I were you only create the label once, and just alter the text when needed as well as just hide the lines you are not using.
Another thing you can do is fill the text in on your label one time, then assign it to an SKCropNode. Then you can use the SKCropNode to bring the font characters back in, which in turn should give you a nicer scrolling effect since you are not doing it at one character at a time.

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?
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.

Add custom "number" images to SKLabelNode in as a Score in SpriteKit with Swift

I basically want to add custom font numbers (with images) in Singles and add them for my score in my game with SpriteKit.
This is the font image style
They are numbers from 0 to 9
I have been trying to figure this out for a while.
I'm not sure if you can add it to SKLabelNode or if you would have to make it in a class.
SKLabelNode would not allow for bitmap fonts. If your custom font was TTF, you would be able to use the custom font with SKLabelNode. An explanation how is here: SKlabelNode custom font
In your case, since you want to use bitmap fonts, you will need to create a class to do it do this. You can either subclass an SKNode or not. But you would want to have one SKNode act as the "root" and then have each digit for your number be an SKSpriteNode (these would be children nodes of the root node representing the number).
One thing you will want to determine is how you want to handle alignment for the number (both horizontally and vertically).
I have used my own custom class to draw numbers before and typically have it configurable to be adjustable horizontally (left justified, right justified, and centered). But out of laziness, I usually always center vertically.
This may sound complicated, but is easy to do. You would just need to have
Way to assign texture atlas of bitmaps for each digit. It is easier
if you have them in progression of 0-9.
Method to set/get number value
Method which can convert
number into individual digits. You'll need to this to compute which
digit maps to which character.
Method to compute width of number. If
your fonts are monospaced, then this is easy. If they are not, then
you'll need to compute width based on digit width and letter-spacing. For monospaced font you still may want to factor in letter-spacing.
Method to compute height of number. This should be easy as 0-9 should
all be the same height.
Method to remove existing SKSpriteNode and
add each new digit. Each new digit being offset by the proper amount
based on alignment.
Your custom class should, upon the number value being set, build the correct SKSpriteNodes to represent the number.
Some updated info for the last question regarding to how one would use an array with the SKTexture. This code is not tested for compilation and is meant to just give you a better idea.
// Example of storing digits in an array. Assumes you are putting this in an atlas and your
// sub-texture names are 0, 1, etc. If not you'll have to adjust the code accordingly
var numTextures = [SKTexture]()
let numberAtlas = SKTextureAtlas(named: "NumberAtlas")
for i in 0 ..< 10 {
numTextures.append(numberAtlas.textureNamed("\(i)"))
}
// To get the texture for a digit
func textureForDigit(digit:Int) -> SKTexture {
return numTextures[digit]
}

Getting the character index for a x,y position in a NSString drawn with drawInRect

I'm currently using NSString's drawInRect to display a font. This is then being uploaded to a texture to be used by OpenGL.
Example:
[String drawInRect:textRect withFont:font lineBreakMode:UILineBreakModeCharacterWrap alignment:UITextAlignmentLeft];
What I now need is a method to determine the index of the character if [x,y] were clicked.
So if I passed in [0,0] it would return index [0]. Or [24,4] would return 5 for example.
One way to do this would be if I can get the individual bounding boxes of the glyphs drawn and iterate through them.
Or is there a better way to draw the text that would allow this sort of functionality?
It is not possible to do this with the NSString and NSAttributedString drawing methods. I'd recommend one of three ways of making tappable text:
1) Core Text PRO: It's fast, it works on older versions of iOS. CON: It's got a lot of boilerplate. You'll have to create NSDictionaries of very wordy text attributes to use when creating attributed strings, then use those attributed strings inside a drawRect: implementation to create a CTFrameSetter via CTFramesetterCreateWithAttributedString(), then use the frame-setter to create a CTFrame, then use the CTFrame to draw your text. Whew! After all that, you can query the CTFrame (through even more complicated code) to find the index of a tapped character at a given CGPoint. All this is not trivial to set up.
2) TextKit PRO: It's much easier than Core Text. CON: It's only available on iOS 7. You can use UITextView, along with the methods exposed by its new properties (an NSLayoutManager and NSTextContainer), to obtain string information given a CGPoint.
3) Use TTTAttributedLabel. PRO: It's easiest of all, and works on older versions of iOS. CON: It may not provide the features you need.
Suggest you use:
NSUInteger count = [string length];
CGFloat widths[length];
for(int i=0; i<count; ++i) {
widths[i] = [string sizeWithFont:font forWidth:10000 lineBreakMode: UILineBreakModeWordWrap].width;
}
and then find the spot by walking this array

Calculate characters that fit a fixed rect TextView

I have seen here people needing to calculate the size of the NSString given a size but I need to do the opposite.
Given a specified rect (or fixed UITextView, or multiline UILabel, no scrolling) I need to know:
if it managed to show all the chars of my NSString
if not, what is the last char shown
So that I can display the remaining text in another UITextView (of course if I could use a single UITextView I would not have this problem).
At first it seems a simple thing to do, but actually I am not finding a way, intuitively I think I could use either UITextView's:
textView.contentSize.height;
or NSString's:
sizeWithFont:constrainedToSize:lineBreakMode:
or a combination of the two, but I need to be precise and those methods do not help me in telling what is the last character that managed to fit the visible area of the UITextView.
Not sure if this is actually possible, but is a requirement of my client who thinks programming iOS is like printing a newspaper and expects to be able to format text around an image....
You could maybe get the maximum height of one line of text from a one character long string.
If you use that with sizeWithFont:constrainedToSize:lineBreakMode: then you should be able to know if your text runs onto more than one line (if the cgsize height is greater than the height of one line of text).
In order to find out the last character (or word) you would have to loop around the length of the string adding characters (or words) as you go and checking for when the cgsize height increases to add a new line, this will give you the character point when to split into multiple strings ( for multiple fields/labels/textviews ) or when to insert line breaks into the text ( if using a single multi-line textview or label ).
I hope you find an easier way...

Resources