I have an NSLayoutManager which is drawing text using the following code:
[[self textLayoutManager] drawGlyphsForGlyphRange: NSMakeRange(0, [[self text] length])
atPoint: textFrame.origin];
in my view's -drawRect:. This works wonderfully, but what I'd really like to be able to do is animate the text in, character by character, as if it were being typed.
I've tried to append characters to a "visible string" variable, then call -[self setNeedsDisplay], but when dealing with text over approximately 20 characters, it begins to lag, as it redraws all of the text every time.
Regression: How can I animate NSLayoutManager's -drawGlyphsForGlyphRange:atPoint:?
(Disclaimer: I don't have a terrible lot of experience with the new APIs, so this is mostly coming from previous experience with text rendering.)
Your major slowdown is going to come from full-on changing the text the layout manager is working with. Even if you're just appending text, replacing the text it's using is going to cause it to throw out all its layout calculations - spacing, needed glyphs, actually reading those glyphs into RAM, applying attributes, etc. - and start over, which gets very expensive very quickly. In terms of actual NSLayoutManager, this is "invalidating the layout".
I see a couple of potential solutions off the top of my head. You could subclass NSLayoutManager ("You can create a subclass of NSLayoutManager to handle additional text attributes, whether inherent or not.") and override the showCGGlyphs:positions:count:font:matrix:attributes:inContext: to progressively ignore certain glyphs (thereby leaving the original text it's using intact). Another approach would be to emulate exactly what happens when you input text in a native container - use a mutable text storage, and append the desired text character-by-character, so that text calculation is done iteratively.
If those alone still don't have great performance, consider using those techniques in tandem with some of the native text views; although this is less true now than in past SDKs, a non-editable UITextView or a UILabel (for mutable and immutable text, respectively) contain far more optimizations than our mere mortal minds could comprehend.
Related
I am trying to draw an NSAttributedString (actually, a constructed NSMutableAttributedString) where the "original" text has been struck and replacement text inserted above it (I'm trying to replicate the look/feel of an Ancient Greek manuscript).
My technique is a combination of NSBaselineOffsetAttributeName with NSKernAttributeName, but it appears that using a negative value for NSKernAttributeName "wipes away" the strikethrough of the text, even if the characters don't overlap.
If I put an extra space after the "A" character (in the original text), the "A" gets the strikethrough, but the "EI" is also offset to the right. So, it appears that the offset/kerning of the "EI" text affects how much of the strikethrough actually occurs.
Here's what I'd like to reproduce (I don't care about the angle; it's not about a picture-perfect reproduction; just the gist):
Here's what is currently happening:
This is when I add an extra space after the strikethrough:
So, the only other thing I can think of would be to render a separate NSAttributedString in the correct place, separate from the current one, but I have no idea how to calculate the location of a specific character in an NSAttributedString when it's drawn. I'm drawing to a PDF, not to any on-screen control like a UILabel. Alternatively, I could draw the "strikethrough" myself as a line, but that seems to still require knowing the coordinates for the text in question, which is calculated on-the-fly, and I hope to use this method to reproduce a large sample of ancient texts, which means doing it by hand just isn't a good answer here.
Anything I'm missing, or any out-of-the-box ideas to try?
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.
I have a font where unfortunately the numbers and letters are different heights. I need to display a reference code which is a mix of letters and numbers and the uneven heights of the characters looks jarring. Is it possible with core text (or another technology on iOS) to render certain characters with a slightly stretched height so that it looks even numbers and letters are displayed together.
E.g i have the string '23Rt59RQ' I need the 2,3,5,9 to be rendered with a larger height.
AFAICT, there's nothing in the CGContext API (which is what you'd want to use for laying out sets of glyphs) which would directly, easily facilitate this.
If it's really very important to use the font you are using, you could make separate calls to CGContextShowGlyphsAtPositions for alphabetic and numeral characters, calling CGContextSetFontSize each time so that the end result ends up matching, but this is a lot of overhead for just drawing text, and will probably result in undesirable performance.
My real advice would be to pick a better font so that this isn't even an issue :)
In the end of used regex to identify the character groups and then created an attributed string varying the font size in the font given in the NSFontAttributeName attribute according to which characters were to be displayed.
Kinda hacky but it had the desired effect.
I need to render an (arbitrarily large) NSAttributedString, in this case ANSI-colored text from an (arbitrarily long) telnet session. The text need not be editable inline. I have explored a few options:
UITextView seems to have by far the best performance and, since I'm targeting iOS 6, it's very easy to use with attributed strings. However, the textview gets progressively slower to render as more text is added, as it hits an HTML DOM parser each time I call setAttributedString: and blocks the UI.
I've tried a few core text rendering frameworks, TTTAttributedLabel and OHAttributedLabel, that also get progressively slower with more text. To be fair, they're labels probably not intended for this sort of thing!
UIWebView (gag) has some issues with rotation and keeping the text properly sized and framed, but I think I could work around it. I can convert my attributed string to HTML and use JavaScript to append (inject) new text as it is received. Surprisingly good performance here.
A friend suggested I think of the user's current scroll position as a viewport into a larger document and (probably with core text) render only the visible part of my attributed string. I'm worried about how this might impact scrolling performance.
So I turn to you, brave interwebs. Ideas for an indie developer? Is a webview my best bet?
You could use a UITableView and split the NSAttributedString into an array of substrings that would each fit a cell's label width. The table view's data source would index into the array of substrings to determine which line of the original string should be placed in each cell.
We have an app that is dependent heavily on kCTParagraphStyleSpecifierParagraphSpacing to manage spacing between paragraphs, which can vary throughout a body of text. For editing performance, we implemented our main Core Text view as a collection of CTFrames that are drawn/redrawn when appropriate.
We've found that if a paragraph uses a nonzero kCTParagraphStyleSpecifierParagraphSpacing as one of its CTParagraphStyleSettings attributes, this paragraph spacing is ignored if that paragraph is the first item in a CTFrame, even if there is a another paragraph preceding it in the text fed to the framesetter.
I suppose this behavior makes sense if you're drawing to a PDF intended to be printed, but given that we're trying to present our text a a single, scrollable and contiguous block of text, it is giving us problems. Is there any way to work around this problem?
If you can't change the behavior with a CTParagraphStyleSetting, I think it's a bug, or Apple thinks that behavior makes sense. Anyway, to get the result that you desire, I think the best way is to use CTTypesetter and handle lineSpacing and paragraphSpacing yourself. I think the CTFrame implementation is quite buggy, as I just run into another not long ago.
For rolling your own solution, you will need CTTypesetterSuggestClusterBreak or CTTypesetterSuggestLineBreak to calculate char count of each line. Line height can be the font size, and you add lineSpacing when drawing each line. When you encounter a newline(\n) character, add paragraphSpacing before drawing next line.
With CTTypesetter, things are more controllable, of course, it also adds some difficulties since you have to handle line breaks and indentation. But this is the only way I can think of to get a more desired result.
Good luck.