Building a CGPath around sentences in UITextView is incredibly slow at -positionFromPosition: - objective-c

I'm doing some text analysis and have run into an annoying performance bump that I can't seem to find how to optimize. I start with the text from a UITextView and split the text into an array of sentences, splitting on characters in ".?!".
Then I loop over each sentence, splitting the sentence into an array of words, and pulling the first and last word from the sentence. With the NSRange of the sentence text in hand, I find the range of the first and last word in the UITextView's text.
The following part is where I get nailed with performance drains. This is how I find the bounding CGRect of the first and last word:
// the from range is increased each iteration
// so i'm not searching the entirety of the text each pass
NSRange range = [textView.text rangeOfString:firstWord options:kNilOptions range:fromRange];
UITextPosition *beginning = textView.beginningOfDocument;
UITextPosition *start = [textView positionFromPosition:beginning offset:range.location];
UITextPosition *end = [textView positionFromPosition:start offset:range.length];
UITextRange *textRange = [textView textRangeFromPosition:start toPosition:end];
firstRect = [textView firstRectForRange:textRange];
I perform this twice, once for the first word and once for the last word.
This works well on smaller text, but approaching 5+ paragraphs Instruments tells me that the UITextView -positionFromPosition: operation is eating up 492ms of clock time, locking up the UI and CPU at 100%.
The thing is I need the CGRect surrounding the first and last words so I can build a CGPath to highlight the sentence. The entire thing works and looks really great, but its the hang while the rects are found that is killing me. I'm fairly new to using UITextView's, so if there is something I can do, either optimizing my searches with ranges or somehow placing my operations on a background thread, I'd be much obliged.

You'll be better off using UITextView's attributedText property, which takes an NSAttributedString. With that, you can set the NSBackgroundColorAttributeName to a colour over a specific range.
Just note the attributed text methods only work in iOS 6+.

Related

Disable line breaking after certain words

I have few UILabels in my app. All of them are set to be multiline by setting numberOfLines as 0. By this, some of them have 1 line, some have 2. My problem is, that according to grammar of language of this app, certain words should never be at the end of an line.
For example, let have sentence: "John is collecting fossils and stamps".
This line will be long enought for line breaking to appear after word "and". According to grammar, this should not happen, so I want to break line before this word, so instead of this after line break:
John is collecting fossils and
stamps
I want to get:
John is collecting fossils
and stamps
Is it possible to achieve this? I am working with iOS 7 and higher, so need not to care with compatibility with older iOS versions.
Solution is to make a subclass of UILabel with 2 methods. Lets assume, that I only want to disable line breaking after word "a" (similar solution can be with multiple words):
- (void)setText:(NSString*)newText {
[super setText:[self fixSpaces:newText]];
}
- (NSString*)fixSpaces:(NSString*)originalText {
NSMutableString* tempString = [[NSMutableString alloc] initWithString:originalText];
[tempString replaceOccurrencesOfString:#"a " withString:#"a " options:NSLiteralSearch range:NSMakeRange(0, tempString.length)];
return tempString;
}
String that will be replaced is "a ", thats a standard 'a' and space. String that will replace it looks the same on the screen, but it is 'a' and non-breaking space (on MAC - Option + Space). Setting any text in a standard way of setting text to UILabel will always work as desired, but comparing string with text of label will not work (but it is easy to fix that by replacing non-breakable spaces with spaces for comparison). Setting text in storyboard or nib will naturally not work.

how to insert extra glyphs?

I want to an UITextView to switch between two display modes.
In mode 1 it should show abbreviations and in the full word in mode 2. For example "Abbr." vs "abbreviation".
What would be the best way to do this? Keeping in mind that some words can have the same abbreviation and that the user is free to type either the full word or the abbreviation?
So far I tried to subclass NSLayoutManager.
Assuming I get an abbreviated string and I have to draw the full word, I would implement the following method:
-(void)setGlyphs:(const CGGlyph *)glyphs
properties:(const NSGlyphProperty *)props
characterIndexes:(const NSUInteger *)charIndexes
font:(UIFont *)aFont
forGlyphRange:(NSRange)glyphRange
{
NSUInteger length = glyphRange.length;
NSString *sourceString = #"a very long string as a source of characters for substitution"; //temp.
unichar *characters = malloc(sizeof(unichar) * length+4);
CGGlyph *subGlyphs = malloc(sizeof(CGGlyph) * length+4);
[sourceString getCharacters:characters
range:NSMakeRange(0, length+4)];
CTFontGetGlyphsForCharacters((__bridge CTFontRef)(aFont),
characters,
subGlyphs,
length+4);
[super setGlyphs:subGlyphs
properties:props
characterIndexes:charIndexes
font:aFont
forGlyphRange:NSMakeRange(glyphRange.location, length+4)];
}
However this method complains about invalid glyph indices "_NSGlyphTreeInsertGlyphs invalid char index" when I try to insert 4 additional glyphs.
You're barking way up the wrong tree; trying to subclass NSLayoutManager in this situation is overkill. Your problem is merely one of swapping text stretches (replace abbrev by original or original by abbrev), so just do that - in the text, the underlying NSMutableAttributedString being displayed.
You say in a comment "some words map to the same abbreviation". No problem. Assuming you know the original word (the problem would not be solvable if you did not), store that original word as part of the NSMutableAttributedString, i.e. as an attribute in the place where the word is. Thus, when you substitute the abbreviation, the attribute remains, and thus the original word is retained, ready for you when you need to switch it back.
For example, given this string: #"I love New York" You can hide the word "New York" as an attribute in the same stretch of text occupied by "New York":
[attributedString addAttribute:#"realword" value:#"New York" range:NSMakeRange(7,8)];
Now you can set that range's text to #"NY" but the attribute remains, and you can consult it when the time comes to switch the text back to the unabbreviated form.
(I have drawn out this answer at some length because many people are unaware that you are allowed to define your own arbitrary NSAttributedString attributes. It's an incredibly useful thing to do.)

cocos2d frame rate lag on dictionary creation and search

I am trying to create a create a simple iPhone game that would throughout the course of running be doing multiple checks to see if user input was a real word. I have a 1.7mb text file (is this a reasonable size?) with each word on its own line containing all of the words in the english language. This is the code that runs in the init method of the game scene. correctWords is an array that will contain all of the users verified word guesses. This code parses through the text file and puts all of the words into an array called currentDict:
correctWords = [[NSMutableArray alloc] init];
//set where to get the dictionary from
NSString *filePath = [[NSBundle mainBundle] pathForResource: [NSString stringWithFormat: #"dictionary"] ofType:#"txt"];
//pull the content from the file into memory
NSData* data = [NSData dataWithContentsOfFile:filePath];
//convert the bytes from the file into a string
NSString* string = [[[NSString alloc] initWithBytes:[data bytes]
length:[data length]
encoding:NSUTF8StringEncoding] autorelease];
//split the string around newline characters to create an array
NSString* delimiter = #"\n";
currentDict = [string componentsSeparatedByString:delimiter];
[currentDict retain];
and then to verify if the word the user inputs is in fact a word I have this check
if([currentDict containsObject: userInput]){
Whenever the game scene loads, there is a very noticeable delay (3-4 seconds) on the device itself, although there it happens almost instantly in the simulator, and then also I have animations running throughout most of the game, but whenever it tries to verify a word, there is a slight but noticeable lag in the animations. I am just wondering if there is a better way to get the dictionary loaded into memory, or if there is some kind of standard practice for verifying words. Also why would checking if it is a word cause a lag in the animation? I had assumed the animation was part of its own thread (and thus would theoretically not be affected)
I would recommend an alternative approach. I don't know how your game works, but it might make sense to give the player a limited set of possible word choices, for example something like Draw Something where there are only so many words you could type; then you would test against far fewer. Before the scene loads, you can have the set of possible words selected from your dictionary then provide letters or options (whatever your game is going) that only allows the user to come up with words that are in that set. Then you can test against a small set.
Another option is to repeat what I've said above frequently throughout your level, so the amount of available words are constantly changing, but load that set periodically when you're not in the middle of an animation or whatever. If there is a brief pause in the game play as the level gets harder, then load new words, or something similar.
That way the real-time game play is not affected by a large dictionary but you can still offer many options throughout the gameplay.
Nothing surprising that comparing thousands of string takes some time and causes lag in animation. You should something read about binary search, hashing, etc. Also loading entire file into NSString and then splitting it is very slow. Your code is just awful, sorry.

NSString drawing vs. String as NSBezierPath drawing

I need to perform efficient hit testing against a (potentially huge) number of components so I've represented all my primitives as NSBezierPath instances. All working great so far.
Now I'm having trouble converting NSString objects, in particular reflecting their position in the view:
I'm using the NSString (BezierConversions) category from Apple's SpeedometerView example to convert strings into bezier paths.
The bezier path created for strings looks great but positioning it to match the position of the NSString instances location in the view doesn't quite work so I suppose this question is really about
NSBezierPath and transformUsingAffineTransform: vs.
a combination of NSAffineTransform applied to a view and NSString drawAtPoint:
In my test project even the trivial case fails:
Grey bezier representation of string drawn using:
NSAffineTransform *moveFinal = [NSAffineTransform transform];
[moveFinal translateXBy:x yBy:y];
[textBezier transformUsingAffineTransform:moveFinal];
and the purple string via
[testString drawAtPoint:NSMakePoint(x, y)
withAttributes:attributes];
Same attributes, same input positions, different location in view.
And it just gets worse with rotated text.
UPDATE #1
Looks like it's boiling down to different bounding boxes returned by
NSString sizeWithAttributes:
NSBezierPath bounds
Now experimenting with NSString boundingRectWithSize
FWIW - Working now.
Using boundingRectWithSize:options:attributes: with the NSStringDrawingUsesDeviceMetrics option gives some good text dimensions to work with, including the actual bounding box occupied by the string when drawn and the offset of the first glyph.
Offset the NSBezierPath returned from bezierWithFont: by that amount and you're good to go..

How do I calculate the exact height of text using UIKit?

I'm using -[NSString sizeWithFont] to get the text height. The character 't' is clearly taller than 'm', but -sizeWithFont returns the same height for both these strings. Here's the code:
UIFont* myFont = [UIFont fontWithName:#"Helvetica" size:1000.0];
NSString* myStr = #"m";
CGSize mySize = [myStr sizeWithFont:myFont];
With 'm' as shown, it returns {834, 1151}. With myStr = #"t" instead, it's {278, 1151}. The smaller width shows up as expected, but not the height.
Does some other function wrap the text tightly? I'm ideally looking for something equivalent to Android's Paint.getTextBounds().
The information you get back from this method is basically the line height for the font (i.e., it's the ascent plus the descent for the font you've chosen). It's not based on individual characters, it's based on specific font metrics. You can get most of the information about a font's metrics from the UIFont class (e.g., the method -ascender gives you the height of the ascender for the font). Mostly, you will be dealing with the total amount of vertical space needed to draw the glyphs with the heights ascenders and the lowest descenders for that font. There is no way to get information about individual glyphs from UIFont. If you need this information, you'll have to look at the CoreText framework, which gives you a lot more flexibility in how you draw and arrange glyphs but is far, far more complicated to use.
For more information on dealing with text in your app, please se the Drawing and Managing Text section of the Text, Web, and Editing Programming Guide. It is also a good launching point for most of the frameworks and classes you'll need to deal with whether you go the UIKit or the CoreText route.
Hmmm... I assume you're only going to be working with individual characters then. sizeWithFont, as noted above, returns the height of the largest character of that font as the text field is going to be that height no matter what you do. You would be able to get the LARGEST height values (UIFont's CGFloat capHeight) however it looks like you're going to be working with all kinds of text
I would take a look at the CoreText framework. Start here and read away. Inevitably you're going to end up with something along the lines of this:
CGFloat GetLineHeightForFont(CTFontRef iFont)
{
CGFloat lineHeight = 0.0;
check(iFont != NULL);
lineHeight += CTFontGetLeading(iFont);
return lineHeight;
}