CoreText mapping characters - iphone

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.

Related

Detecting tapped character. UITextView characterRangeAtPoint always returns nil

I'm trying to determine, which particular character in UITextView has been tapped. I tried to use characterRangeAtPoint: method. But it always returns nil, wherever in the UITextView I tap.
I even wrote and run the following piece of code:
for (int x = 0; x<1000; x++) {
pos.x = x;
for (int y = 0; y<1000; y++) {
pos.y = y;
UITextRange * range = [textView characterRangeAtPoint:pos];
if (range!=nil) {
NSLog(#"x: %f, y: %f", pos.x, pos.y);
}
}
}
Well, it never reaches the NSLog string.
What am I doing wrong?
This is an iOS 7 bug. characterRangeAtPoint: always returns nil, unless some text has been selected at least once beforehand.
As a workaround, you could call setSelectedRange: or setSelectedTextRange: beforehand, but then the textView would highlight your selection. I came up with the following solution:
[textView select:textView];
[textView setSelectedTextRange:nil];
After you call these two methods, characterRangeAtPoint: will start working as expected.
Note that you just need to call this once after textView has been initialized.
EDIT: my answer was edited from "unless some text has been selected beforehand" to "unless some text is currently selected". This is wrong: text doesn't need to be currently selected, it just has to be selected once. All subsequent calls to setSelectedRange: will then be successful, as stated in my note. Edited back for further clarity.
Your UITextView must be selectable. In storyboard, check the "Selectable" checkbox.
In code,
textView.selectable = YES;
This is also required for the -closestPositionToPoint: method on UITextView.
Use shouldChangeCharactersInRange method instead of characterRangeAtPoint.
And use your If condition inplace of loop statement.
Is your TextView user-editable?
If YES, then try the texViewDidChangeSelection method. It is invoked every time the caret(cursor) is moved in a textView. Then get the cursor position by textView.selectedRange.location to point to the index, of the caret.
My assumption here is that you want the location of the character with respect to the number of character, and not co-ordinates in 2D.

can't get CALayer containsPoint to work

I've got an array of CALayers containing images which can be moved around by the user, and i'm trying to use containsPoint to detect if they have been touched - the code is as follows:
int num_objects = [pageImages count];
lastTouch = [touch locationInView:self];
CGRect objRect;
CALayer *objLayer;
for (int i = 0; i < num_objects; i++) {
objLayer = [pageImages objectAtIndex:i];
objRect = objLayer.bounds;
NSLog(#"layerPos:%#, layerBounds:%#", NSStringFromCGPoint(objLayer.position), NSStringFromCGRect(objRect));
NSLog(#"point:%#", NSStringFromCGPoint(lastTouch));
if ([objLayer containsPoint:lastTouch] == TRUE) {
NSLog(#"touched object %d", i);
return i;
}
}
The information i'm outputting puts the touch within the bounds of the layer (i've assumed position is the centre of the layer, i haven't altered the anchor point. The layer hasn't been rotated or anything like that either), but containsPoint: doesn't return true. Can anyone see what i'm doing wrong, or suggest a different/better way to achieve what i want?
So .. found the problem - the point needs to be converted from superlayer coordinates in order to work with the layer containsPoint:
replace
if ([objLayer containsPoint:lastTouch] == TRUE) {
with
if ([objLayer containsPoint:[objLayer convertPoint:lastTouch fromLayer:objLayer.superlayer]] == TRUE) {
You can mess about with the co-ordinates yourself and use CGRectContainsPoint: (see comments above), but this is a simpler solution so i get to answer my own question for the first time. big tick for me, yay!

Core Text: counting pages in background thread

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;
}

UISlider how to set the initial value

I'm pretty new at this iphone dev stuff and i'm working on some existing code at a company. The uislider i'm trying to set the initial value on is actually in a UITableViewCell and is a custom control. I was thinking in the cell init
cell = (QuantitiesCell *)[self loadCellWithNibName:#"QuantitiesCell" forTableView:ltableView withStyle:UITableViewCellStyleDefault];
i could just call something like
((QuantitiesCell *)cell).value = 5;
The actual QuantitiesCell class has the member value and the following functions
-(void)initCell;{
if (listOfValues == nil) {
[self initListOfValues];
}
quantitiesSLider.maximumValue = [listOfValues count]-1;
quantitiesSLider.minimumValue = 0;
quantitiesSLider.value = self.value;
}
-(void)initListOfValues;{
listOfValues = [[NSMutableArray alloc] initWithCapacity:10];
int j =0;
for (float i = minValue; i <= maxValue+increment; i=i+increment) {
[listOfValues addObject:[NSNumber numberWithFloat: i]];
if (i == value) {
quantitiesSLider.value = j;
}
j++;
}
}
like i said, i'm pretty new at this so if i need to post more of the code to show whats going to get help, let me know,
This slider always is defaulting to 1, the slider ranges usually from 1-10, and i want to set it to a specific value of the item i'm trying to edit.
Setting slider.value is the correct way if your linkage is correct (you have made the connections in Interface Builder). If you have created a UISlider in code, all you have to do is set the value property. If that is not working then be sure firstly that the object is "live" (correctly allocated, not released, not out of scope etc.) and secondly that the functions in which you set slider.value are actually being called.
If you are using Interface Builder and are not sure of how to connect your slider as an IBOutlet, you should check out iTunes University - search for "CS193P" to find some excellent videos from Stanford University. The first couple will take you through making those connections. If using IB and you have not made the connection - nothing will happen.
PS I had the same issuer - you need to add the UISlide view first then you can change the value.

Cursor location in a uitextfield

im using customkeyboard in my controller. but how to get the cursor location in the uitextfiled. my requirement is to enter a charecter in a desired location of textfield. is it possible?
Let's assume that you code is in a method on an object where self.textField is the UITextField in question.
You can find the current position of the cursor/selection with:
NSRange range = self.textField.selectedTextRange;
If the user has not selected text the range.length will be 0, indicating that it is just a cursor. Unfortunately this property does not appear to be KVO compliant, so there is no efficient way to be informed when it changes, however this should not be a problem in your case because you probably really only care about it when you are responding to user interaction with your custom keyboard.
You can then use (assuming newText holds the input from your custom keyboard).
[self.textField replaceRange:range withText:newText];
If you need to subsequently adjust the cursor/selection you can use:
self.textField.selectedTextRange = newRange;
For example, you may want to position the cursor after the text you inserted.
UPDATE:
In my original answer I failed to notice that I was leveraging a category I had added to UITextView:
- (void)setSelectedRange:(NSRange)selectedRange
{
UITextPosition* from = [self positionFromPosition:self.beginningOfDocument offset:selectedRange.location];
UITextPosition* to = [self positionFromPosition:from offset:selectedRange.length];
self.selectedTextRange = [self textRangeFromPosition:from toPosition:to];
}
- (NSRange)selectedRange
{
UITextRange* range = self.selectedTextRange;
NSInteger location = [self offsetFromPosition:self.beginningOfDocument toPosition:range.start];
NSInteger length = [self offsetFromPosition:range.start toPosition:range.end];
NSAssert(location >= 0, #"Location is valid.");
NSAssert(length >= 0, #"Length is valid.");
return NSMakeRange(location, length);
}
Then replace use self.textField.selectedRange instead of self.textField.selectedTextRange and proceed as I described.
Thanks to omz for pointing out my error.
Of course, you can work directly with UITextRange but, at least in my case, this proved to be rather ungainly.
The answer is that you can't get the current cursor location for all types of editing that can be done with the textfield. You can insert characters at the cursor with [textField paste], but the user can move the cursor, select and modify text, without a way to get notified where the cursor ended up.
You can temporarily paste a special character and search its position in the string, remove it, and then add the character you want to have there.
Swift
Get the cursor location:
if let selectedRange = textField.selectedTextRange {
let cursorPosition = textField.offsetFromPosition(textField.beginningOfDocument, toPosition: selectedRange.start)
}
Enter text at some arbitrary location:
let arbitraryValue: Int = 5
if let newPosition = textField.positionFromPosition(textField.beginningOfDocument, inDirection: UITextLayoutDirection.Right, offset: arbitraryValue) {
textField.selectedTextRange = textField.textRangeFromPosition(newPosition, toPosition: newPosition)
textField.insertText("Hello")
}
My full answer is here.