Core Text: counting pages in background thread - iphone

Let's say I'm writing text viewer for the iPhone using Core Text. Every time user changes base font size I need to count how many pages (fixed size CGRects) are needed to display the whole NSAttributedString with given font sizes.
And I would like to do this in separate NSOperation, so that user does not experience unnecessary UI lags.
Unfortunately, to count pages I need to draw my frames (CTFrameDraw) using invisible text drawing mode and then use CTFrameGetVisibleStringRange to count characters. But to draw a text I need a CGContext. And here the problems begin...
I can obtain a CGContext in my drawRect by calling UIGraphicsGetCurrentContext, but in this case:
I have to call any method that operates on the CGContext using performSelectorOnMainThread, right?
The other thread should CFRetain this context. Is it acceptable to use drawRect's CGContext outside drawRect method?
Any other solutions? Creating separate CGContext in the worker thread? How? CGBitmapContext? How can I be sure that all conditions (i don't know, resolution? etc.) will be the same as in drawRect's CGContext, so that pages will be counted correctly?

You don't need to CTFrameDraw before getting result from CTFrameGetVisibleStringRange

You can use CTFramesetterSuggestFrameSizeWithConstraints.
See my question here: How to split long NSString into pages

use CTFramesetterSuggestFrameSizeWithConstraints ,if you specify the parameter of fitRange,it return the actual range of the string
+ (NSArray*) pagesWithString:(NSString*)string size:(CGSize)size font:(UIFont*)font;
{
NSMutableArray* result = [[NSMutableArray alloc] initWithCapacity:32];
CTFontRef fnt = CTFontCreateWithName((CFStringRef)font.fontName, font.pointSize,NULL);
CFAttributedStringRef str = CFAttributedStringCreate(kCFAllocatorDefault,
(CFStringRef)string,
(CFDictionaryRef)[NSDictionary dictionaryWithObjectsAndKeys:(id)fnt,kCTFontAttributeName,nil]);
CTFramesetterRef fs = CTFramesetterCreateWithAttributedString(str);
CFRange r = {0,0};
CFRange res = {0,0};
NSInteger str_len = [string length];
do {
CTFramesetterSuggestFrameSizeWithConstraints(fs,r, NULL, size, &res);
r.location += res.length;
[result addObject:[NSNumber numberWithInt:res.length]];
} while(r.location < str_len);
CFRelease(fs);
CFRelease(str);
CFRelease(fnt);
return result;
}

Related

Calculating enough text to fit within existing UILabel

I can't get some CoreText text wrapping code working for me; it's just too complicated. I'm going to try and go another route, which is to split my UILabel into two.
What I'm trying to achieve is to have my text appear to wrap around my fixed sized rectangular image. It'll always be the same dimensions.
So, when the UILabel next to the image fills up exactly, it'll create another UILabel below the image.
Now, how do I calculate the text in the first UILabel and have it fit nicely in the entire width of the UILabel, without being too short or cut off at the end?
Well, this ought to work to get the substring of the master string that will fit within the desired width:
//masterString is your long string that you're looking to break apart...
NSString *tempstring = masterString;
while (someLabel.bounds.size.width < [tempString sizeWithFont:someLabelLabel.font].width) {
NSMutableArray *tempArray = [NSMutableArray arrayWithArray:[tempString componentsSeparatedByString:#" "]];
//Remove the last object, which is the last word in the string...
[tempArray removeLastObject];
//Recreate the tempString with the last word removed by piecing the objects/words back together...
tempString = #"";
for (int i=0; i < tempArray.count - 1; i++) {
tempString = [tempString stringByAppendingFormat:#"%# ", [tempArray objectAtIndex:i]];
}
//You must append the last object in tempArray without the space, or you will get an infinite loop...
tempString = [tempString stringByAppendingFormat:#"%#", [tempArray objectAtIndex:tempArray.count - 1]];
}
//Now do whatever you want with the tempString, which will fit in the width desired...
Of course, this is assuming you want the separation to occur using word wrapping. If you don't mind words themselves being cut apart (i.e. character wrap) in order to fully take up the desired width, do this instead:
NSString *tempstring = masterString;
while (someLabel.bounds.size.width < [tempString sizeWithFont:someLabelLabel.font].width) {
tempString = [tempString substringToIndex:tempString.length - 1];
}
//Now do whatever you want with the tempString, which will fit in the width desired...
In order to get the remaining piece of the string left over, do this:
NSString *restOfString = [masterString substringFromIndex:tempString.length];
Hope this helps. I have to admit that I haven't properly tested this code yet, though I've done something similar in the past...
Try below link its will help you.
If you want to create a "link" on some custom text in your label, instead of using a WebView as #Fabian Kreiser suggested, you sould use my OHAttributedLabel class (you can find it this link)
See the sample code provided on my github repository: you can use my addCustomLink:inRange: method to add a link (with a customized URL) to a range of text (range that you could determine by iterating over every occurrences of the word "iPhone" in your text very easily). Then in the delegate method on OHAttributedLabel, you can catch when the link is tapped and act accordingly to do whatever you need.

Animated-GIF images received as NSData through NSSocket unable to split frames

I'm having trouble with the way iOS handles animated-GIF's. I know that you cannot use animated-GIF's on an UIImageView and you have to use custom animations on the UIImageView.
But..
I have a Java server that sends GIF images through a socketstream. The iOS (iPhone) receives that stream and converts it into an NSData type. I've succeeded in capturing and displaying this image in a UIImageView but as many of you already know.. it only displays the first frame.
I Also found some code to decode that GIF into separate images, but that code works from a GIF file and not NSData.
Question: How do i convert the NSData file into separate images and place them in an NSArray to use it as animation?
Note: In the NSData that is received are both an image and some text separated by a rare character. so the NSData looks like this: [image] [separator] [text].
Hope somebody can give me some pointers or some samples to work with..
Thanks in advance, i will keep searching untill you or me finds an answer :)
Unless you're targeting devices before IOS 4, use ImageIO.
NSMutableArray *frames = nil;
CGImageSourceRef src = CGImageSourceCreateWithData((CFDataRef)data, NULL);
if (src) {
size_t l = CGImageSourceGetCount(src);
frames = [NSMutableArray arrayWithCapacity:l];
for (size_t i = 0; i < l; i++) {
CGImageRef img = CGImageSourceCreateImageAtIndex(src, i, NULL);
if (img) {
[frames addObject:[UIImage imageWithCGImage:img]];
CGImageRelease(img);
}
}
CFRelease(src);
}
I made a wrapper that also handles animation time based on the code from Anomie
https://gist.github.com/3894888

CoreText mapping characters

I have some in a touch handler which responds to a tap on a view that I've drawn some attributed text in. through this, I've got to the point where I have a CTRunRef (and the associated line) as well as the number of glyphs in that run.
What I'm not able to figure out easily, is how I can take that run of glyphs and, given my attributed string, map it out to characters in the string.
Specifically the problem is I would like to know what word the user tapped on in the view, so I can process whether or not that word is a URL and fire off a custom delegate method so I can open a web view with it. I have all the possible substrings, I just don't know how to map where the user tapped to a particular substring.
Any help would be greatly appreciated.
UPDATE: I've actually gone and done it a different way, on the suggestion of another person off of stackoverflow. Basically what I've done is to set a custom attribute, #"MyAppLinkAddress" with the value of the URL I found when I was converting the string to an attributed string. This happens before I draw the string. Therefore, when a tap event occurs, I just check if that attribute exists, and if so, call my delegate method, if not, just ignore it. It is working how I'd like now, but I'm going to leave this question open for a few more days, if someone can come up with an answer, I'll happily accept it if its a working solution so that some others may be able to find this information useful at some point in the future.
So as I mentioned in the update, I elected to go a different route. Instead I got the idea to use a custom attribute in the attributed string to specify my link, since I had it at creation time anyway. So I did that. Then in my touch handler, when a run is tapped, I check if that run has that attribute, and if so, call my delegate with it. From there I'm happily loading a webview with that URL.
EDIT: Below are snippets of code explaining what I did in this answer. Enjoy.
// When creating the attribute on your text store. Assumes you have the URL already.
// Filled in for convenience
NSRange urlRange = [tmpString rangeOfString:#"http://www.foo.com/"];
[self.textStore addAttribute:(NSString*)kCTForegroundColorAttributeName value:(id)[UIColor blueColor].CGColor range:urlRange];
[self.textStore addAttribute:#"CustomLinkAddress" value:urlString range:urlRange];
then...
// Touch handling code — Uses gesture recognizers, not old school touch handling.
// This is just a dump of code actually in use, read through it, ask questions if you
// don't understand it. I'll do my best to put it in context.
- (void)receivedTap:(UITapGestureRecognizer*)tapRecognizer
{
CGPoint point = [tapRecognizer locationInView:self];
if(CGRectContainsPoint(textRect, point))
{
CGContextRef context = UIGraphicsGetCurrentContext();
point.y = CGRectGetHeight(self.contentView.bounds) - kCellNameLabelHeight - point.y;
CFArrayRef lines = CTFrameGetLines(ctframe);
CFIndex lineCount = CFArrayGetCount(lines);
CGPoint origins[lineCount];
CTFrameGetLineOrigins(ctframe, CFRangeMake(0, 0), origins);
for(CFIndex idx = 0; idx < lineCount; idx++)
{
CTLineRef line = CFArrayGetValueAtIndex(lines, idx);
CGRect lineBounds = CTLineGetImageBounds(line, context);
lineBounds.origin.y += origins[idx].y;
if(CGRectContainsPoint(lineBounds, point))
{
CFArrayRef runs = CTLineGetGlyphRuns(line);
for(CFIndex j = 0; j < CFArrayGetCount(runs); j++)
{
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
NSDictionary* attributes = (NSDictionary*)CTRunGetAttributes(run);
NSString* urlString = [attributes objectForKey:#"CustomLinkAddress"];
if(urlString && ![urlString isEqualToString:#""])
{
[self.delegate didReceiveURL:[NSURL URLWithString:urlString]];
UIGraphicsPopContext();
return;
}
}
}
}
UIGraphicsPopContext();
}
}
After you find the tapped line, you can ask for the index in string by calling CTLineGetStringIndexForPosition(). There's no need to access individual runs.

Convert a string + a number to an onject

I am new to Obj C, but familiar with OOP. I am working on a game that uses collision detection to score points and change UIImageView properties like background color.
I have a finite number of UIImageViews that are subviews of a larger view. I can manually check for collision and set the background colors of the UIImageViews accordingly. The problem is that this creates a rather large if / else block that I can see growing out of control. In JavaScript I can use a for loop and eval() to evaluate a string + i (the loop var) and then simply call the new var.backgroundColor = someColor;
It seems that eval is not used in OBJ C, so I am looking for an equivalent that will allow me to create a loop that executes the same block of code on a given object that only differs from other objects by name only:
Example:
if (someStatement == true){
object_01.someProperty = newProperty;
} else if (someOtherStatement == true){
object_02.someProperty = newProperty;
} else if (someOtherOtherStatement == true){
object_03.someProperty = newProperty;
}
I want to write the above as a loop. I realize I can use an Array, but if so, how? Also, I want to loop through CGRects which are not storing properly in an Array.
Any help greatly appreciated!
Damon
Creating your array:
NSMutableArray *items = [NSMutableArray array];
Populating your array with CGRects:
[items addObject:[NSValue valueWithCGRect:frame]];
Pulling a CGRect from the array:
CGRect frame = [[items objectAtIndex:0] CGRectValue];
Replacing an index in the array:
[items replaceObjectAtIndex:0 withObject:[NSValue valueWithCGRect:frame]];
The best way is to put object_01, object_02, ... into an array and then use `-[NSArray objectAtIndex:] to access the objects.
You could also use Key-Value Coding:
NSString *keyPath = [NSString stringWithFormat:#"object_0%d.someProperty", objectIndex];
[self setValue:newProperty forKeyPath:keyPath];
But as rpetrich mentioned, it's not good programming style.

Preload Images in iPhone application?

I have a sequence of images needed to display in a short time (PNG sequence). There are totally 31 PNGs in total, each with file size about 45KB. I have already loaded them with the following codes:
imgArray = [[NSMutableArray alloc] init];
for(int i = 0; i <= 30; i++) {
NSString * filename = [NSString stringWithFormat:#"img_000%.2d.png", i];
UIImage *temp = [UIImage imageNamed:filename];
[imgArray addObject:temp];
[temp release];
temp = nil;
}
I use the following codes for displaying the images:
CGImageRef image = [(UIImage *)[imgArray objectAtIndex:imgFrame] CGImage];
imgLayer.contents = (id)image;
if(imgFrame < 29) {
imgFrame++;
} else {
imgFrame = 0;
imgLayer.hidden = TRUE;
[imgTimer invalidate];
}
where imgLayer is a CALayer. (imgTimer is a repeating timer with interval 0.03s)
But I found that when I call the images out, it is very laggy at the first time. Except the 1st appearance, other appearance has no problem.
Is it related to preloading images? Or are my images too big in file size?
The reason for your lags are hard to tell without profiling data. But here is a trick that might help: Join all your images into one large file. Try to make it rectangular (maybe 6x6 in your case or 4*8). Then load this single file and crop each image out for display (i.e. create an image for display with the size of a tile and copy one tile from the big image after the other into the display image).
Images are loaded when they are used and displayed.
If you use the time profiler in Instruments, you will the lag you're experiencing. If you then zoom to that lag and look at what is causing it, you will usually see that "copyImageBlockSetPNG" is the function taking time, right before "inflate".
What you must find a way to do is create your images and force load them before you need them. That's another story apparently.