2010-09-27 14 views
9

Tengo un código en una vista que dibuja texto atribuido utilizando CoreText. En esto, estoy buscando URL y haciéndolas azules. La idea es no incluir todos los gastos generales de un UIWebView solo para obtener enlaces clicables. Una vez que un usuario toca en ese enlace (no en toda la celda de la vista de tabla), deseo activar un método delegado que luego se usará para presentar una vista modal que contiene una vista web que va a esa url.Uso de CoreText y toques para crear una acción seleccionable

Estoy guardando la ruta y la cadena en sí misma como variables de instancia de la vista, y el código de dibujo ocurre en -drawRect: (lo dejé para abreviar).

Sin embargo, mi controlador táctil, si bien está incompleto, no imprime lo que esperaba. Es a continuación:

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

no es el código más bonito en este momento, todavía estoy tratando de hacer que funcione solo, por lo que valga la calidad del código.

El problema que tengo es que cuando toco fuera del enlace, lo que espero que suceda, sucede: Nada se dispara.

Sin embargo, yo esperaría a "Tapped line" se imprimen si tocar en la misma línea el enlace está activado, lo cual no sucede, y yo esperaría tanto "Tapped line" y "Tapped run" que se imprimen si hago tapping en la URL.

No estoy seguro de dónde llevar esto más allá, los recursos que he examinado para resolver este problema son específicos de Cocoa (que es casi completamente inaplicable) o carecen de información sobre este caso específico.

Con mucho gusto le mostraré los punteros a la documentación que detalla cómo detectar correctamente si ocurrió un toque dentro de los límites de un texto central que dibuja el código, pero en este punto, solo quiero resolver este problema, por lo que cualquier La ayuda sería muy apreciada.

ACTUALIZACIÓN: He reducido mi problema a un problema de coordinación. He dado la vuelta a las coordenadas (y no como se muestra arriba) y el problema que tengo es que toca registrarme como era de esperar, pero el espacio de coordenadas está volteado, y parece que no puedo voltearlo.

+0

¿Ha tenido en cuenta el hecho de que CoreText utiliza un sistema de coordenadas volteado? En este momento me parece que estás comparando cosas en dos sistemas de coordenadas diferentes. – Jacques

+0

Además, es una mala idea recrear el fotocomponedor en cada toque así. Crear un conjunto de marcos es muy costoso, por lo que debe almacenarlo en la memoria caché al momento de dibujar o configurar el texto. – Jacques

+0

Mi metodología de desarrollo es simple: 1) Hacer que funcione; 2) Hacerlo bien; 3) Hazlo rápido/limpio. Vamos a tratar con el # 2 cuando tengamos el # 1 en funcionamiento :) También con respecto al sistema de coordenadas, sí me doy cuenta de eso, y por un tiempo no lo estaba manejando.Ahora, en el código, después de consultar con un colega, él me puso en línea con esto, y al menos ahora está detectando los grifos en las líneas, pero no las ejecuciones. Todavía estoy tratando de resolverlo. – jer

Respuesta

8

Acabo de hacer esto para obtener el índice de caracteres de la cadena desde la posición táctil. El número de línea sería i en este caso:

versión
- (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); 
} 
+0

Gracias Nick me salvaste! :) – stefanosn

+0

Esto funciona muy bien, puedo comparar esto con un conjunto de rangos que he almacenado para palabras topables. Esto es mucho más rápido que mi antiguo enfoque. Gracias :) –

+0

¿Cómo puedo obtener ctFrame? –

0

Puede tratar de agregar su dibujo de texto en un CALayer, agregar esta nueva capa como subcapa de su capa de vistas y hacer una prueba en sus toques? Si lo haces, deberías poder crear un área táctil más grande haciendo que la capa sea más grande que el texto dibujado.

// hittesting 
UITouch *touch = [[event allTouches] anyObject]; 
touchedLayer = (CALayer *)[self.layer hitTest:[touch locationInView:self]]; 
+0

Entonces tendría otro problema de tener que dividir mi texto en dos capas diferentes: la que quiero activar para el delegado, y luego el resto. El problema con el que me encontré allí es qué pasa si la URL está en el medio de una línea y hay texto antes y después. Entonces tendría que dividirme en muchas capas diferentes. Podría ponerse peludo si hay unas pocas URL en el grupo de texto que represento. Prefiero simplemente buscar una lista de todos los elementos de la cadena attrib que he coloreado en azul, y obtener sus cuadros delimitadores, ver si el usuario tocó allí y llamar al delegado con esa url. – jer

+0

Es cierto que esto podría ser un problema. Tal vez no entendí bien tu problema. ¿Qué tal si conservamos el texto tal como está y superponemos una capa golpeable para atrapar estos toques? – msg

+0

Aún necesito saber exactamente dónde colocarlo, lo que significa que aún necesito obtener el rect de la URL. Eso me devuelve al punto de partida. :) – jer

0

Swift 3 de la respuesta de Nick H247:

var ctFrame: CTFrame? 

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { 
    guard let touch = touches.first, let ctFrame = self.ctFrame else { return } 

    let pnt = touch.location(in: self.view) 
    let reversePoint = CGPoint(x: pnt.x, y: self.frame.height - pnt.y) 
    let lines = CTFrameGetLines(ctFrame) as [AnyObject] as! [CTLine] 

    var lineOrigins = [CGPoint](repeating: .zero, count: lines.count) 
    CTFrameGetLineOrigins(ctFrame, CFRange(location: 0, length: 0), &lineOrigins) 

    for (i, line) in lines.enumerated() { 
     let origin = lineOrigins[i] 

     if reversePoint.y > origin.y { 
      let index = CTLineGetStringIndexForPosition(line, reversePoint) 
      print("index \(index)") 
      break 
     } 
    } 
} 
Cuestiones relacionadas