Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using CoreText and touches to create a clickable action

I have some code in a view which draws some attributed text using CoreText. In this, I'm searching for urls and making them blue. The idea is to not bring in all the overhead of a UIWebView just to get clickable links. Once a user taps on that link (not the whole table view cell), I want to fire off a delegate method which will then be used to present a modal view which contains a web view going to that url.

I'm saving the path and the string itself as instance variables of the view, and the drawing code happens in -drawRect: (I've left it out for brevity).

My touch handler however, while incomplete, is not printing what I'd expect it to. It is below:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint point = [touch locationInView:self];
    CGContextRef context = UIGraphicsGetCurrentContext();

    NSLog(@"attribString = %@", self.attribString);
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)self.attribString);
    CTFrameRef ctframe = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, self.attribString.length), attribPath, NULL);

    CFArrayRef lines = CTFrameGetLines(ctframe);
    for(CFIndex i = 0; i < CFArrayGetCount(lines); i++)
    {
        CTLineRef line = CFArrayGetValueAtIndex(lines, i);
        CGRect lineBounds = CTLineGetImageBounds(line, context);

        // Check if tap was on our line
        if(CGRectContainsPoint(lineBounds, point))
        {
            NSLog(@"Tapped line");
            CFArrayRef runs = CTLineGetGlyphRuns(line);
            for(CFIndex j = 0; j < CFArrayGetCount(runs); j++)
            {
                CTRunRef run = CFArrayGetValueAtIndex(runs, j);
                CFRange urlStringRange = CTRunGetStringRange(run); 
                CGRect runBounds = CTRunGetImageBounds(run, context, urlStringRange);

                if(CGRectContainsPoint(runBounds, point))
                {
                    NSLog(@"Tapped run");
                    CFIndex* buffer = malloc(sizeof(CFIndex) * urlStringRange.length);
                    CTRunGetStringIndices(run, urlStringRange, buffer);
                    // TODO: Map the glyph indices to indexes in the string & Fire the delegate
                }
            }
        }
    }
}

It's not the prettiest code at the moment, I'm still trying to just make it work, so forgive the code quality.

The problem I'm having is that when I tap outside the link, what I expect would happen, happens: Nothing gets fired.

However, I would expect "Tapped line" to get printed if I tap the same line the link is on, which doesn't happen, and I would expect both "Tapped line" and "Tapped run" to get printed if I tap on the URL.

I'm unsure as to where to take this further, the resources I've looked at for resolution to this issue are Cocoa specific (which is almost completely inapplicable), or lacking information on this specific case.

I'll gladly take pointers to documentation which detail how to properly go about detecting if a touch occurred within the bounds of a core text drawing over code, but at this point, I just want to resolve this problem, so any help would be greatly appreciated.

UPDATE: I have narrowed down my problem to a coordinate issue. I have flipped the coordinates (and not as shown above) and the problem I'm getting is that touches register as I'd expect, but the coordinate space is flipped, and I can't seem to flip it back.

like image 906
jer Avatar asked Sep 27 '10 09:09

jer


1 Answers

I have just done this to get the string character index from the touch position. The line number would be i in this case:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"touch ended");

    UITouch* touch = [touches anyObject];

    CGPoint pnt = [touch locationInView:self];

    CGPoint reversePoint = CGPointMake(pnt.x, self.frame.size.height-pnt.y);

    CFArrayRef lines = CTFrameGetLines(ctFrame);

    CGPoint* lineOrigins = malloc(sizeof(CGPoint)*CFArrayGetCount(lines));

    CTFrameGetLineOrigins(ctFrame, CFRangeMake(0,0), lineOrigins);

    for(CFIndex i = 0; i < CFArrayGetCount(lines); i++)
    {
        CTLineRef line = CFArrayGetValueAtIndex(lines, i);

        CGPoint origin = lineOrigins[i];
        if (reversePoint.y > origin.y) {
            NSInteger index = CTLineGetStringIndexForPosition(line, reversePoint);
            NSLog(@"index %d", index);
            break;
        }
    }
    free(lineOrigins);
}
like image 171
Nick Hingston Avatar answered Nov 20 '22 09:11

Nick Hingston