Creación de un editor de text para iOS 7

Problema

Necesito entender cómo funciona TextKit y cómo puedo usarlo para crear un editor de text. Necesito averiguar cómo dibujar SOLO el text visible con el que interactúa el usuario final o determinar cómo iría aplicando perezosamente los attributes al text visible solo sin aplicar attributes a todo el range de text modificado en el método de edición de processs.

Fondo

iOS 7 salió con TextKit. Tengo un tokenizador y un código que implementa completamente TextKit (consulte el proyecto TextKitDemo de Apple, se proporciona un enlace a continuación) … y funciona. Sin embargo, es REALMENTE lento. A medida que el text es analizado, por el NSTextStorage, se requiere colorear el range ENTERO del text editado, en el método processEditing, en el mismo hilo. Descargar el trabajo en un hilo no ayuda. Simplemente es muy lento. Llegué al punto en que puedo volver a atribuir solo los ranges modificados, pero si el range es demasiado grande, el process será lento. En algunas condiciones, todo el documento podría ser invalidado después de que se haya realizado un cambio.

Aquí hay algunas ideas que tengo. Por favor, hágamelo saber si alguno de estos funcionará o tal vez me empuje en la dirección correcta.

1) Varios contenedores NSText

Al leer los documentos, parece que puedo agregar varios NSTextContainers dentro de un NSLayoutManager. Estoy asumiendo que, al hacer esto, debería ser capaz de definir no solo el número de líneas que se pueden dibujar en el NSTextContainer, sino también saber qué NSTextContainer es visible para el usuario final. Sé que si voy por esta ruta, tendré que invertir MUCHO time para ver si es factible. Las testings iniciales sugieren que solo necesita un NSTextContainer. Por lo tanto, tendría que subclass NSLayout o crear un contenedor donde el administrador de layout determina qué text va a qué contenedor de text. Por que Además, no tengo ni idea de cómo TextKit me hace saber que es hora de dibujar un NSTextContainer en particular … ¡tal vez no funcione así!

2) Rangos inválidos con NSLayoutManager

Invalidar el layoutManager utilizando invalidateLayoutForCharacterRange: actualCharacterRange :. Pero, ¿qué hace esto y cómo downloadá la fase de atribución del text? ¿Cuándo me avisa que un text en particular necesita ser resaltado? Además, veo que el NSLayoutManager dibujará perezosamente los glifos … ¿cómo? cuando? ¿Cómo me ayuda esto? ¿Cómo presiono esta llamada para que pueda atribuir la cadena de respaldo antes de que exponga el text?

3) Sobresaliendo el NSLayoutManager drawGlyphsForGlyphRange: atPoint: method.

Realmente no quiero hacer esto. Dicho esto, en Mac OS X, NSAttributedStrings tiene este concepto de attributes temporales donde la información de estilo solo se usa para la presentación. Esto acelera el process de resaltar GRANDEMENTE! El problema es que no existe en el marco de trabajo de iOS 7 TextKit (o está ahí y no lo sé). Creo que superando este método me dará el mismo tipo de velocidades que obtendría al usar attributes temporales … ya que podría responder a todas las preguntas de layout, color y formatting en este método sin siquiera tocar el NSTextStorage atribuido string. El único problema es que no sé cómo funciona este método en relación con otros methods proporcionados en la class NSLayoutManager. ¿Mantiene el estado del ancho y la altura? ¿Modifica el tamaño del NSTextContainer cuando es demasiado pequeño? Además, solo dibuja glifos para los caracteres que se han agregado en el búfer de text. No vuelve a dibujar toda la pantalla. Solo una pequeña parte … y eso está perfectamente bien. Tengo algunas ideas sobre cómo podría trabajar con esto … pero realmente no tengo ningún deseo de diseñar los glifos. Eso es WAY demasiado trabajo y no he encontrado un buen ejemplo que lo haga.

Le agradecería mucho cualquier ayuda que tenga para ofrecer.

Como agradecimiento, estoy enumerando todos los frameworks y references que he utilizado en los últimos años que me han ayudado a llegar a donde estoy ahora con la esperanza de que sean útiles para usted.

Marcos de resaltado de syntax:

  • http://colorer.sourceforge.net/
  • https://github.com/MikeJ1971/Glint (No es compatible con cadenas, comentarios, etc.)
  • https://projects.gnome.org/gtksourceview/features.html (Proporciona una liberación decente para la generación de tokens. Funciona con GTK pero podría reescribirse para un administrador de layout diferente)
  • http://parsekit.com/ (Buen analizador. Sin embargo, debe ajustar la API para crear una máquina de estado cuando necesite reparar ranges)
  • http://svn.gna.org/viewcvs/etoile/trunk/Etoile/Languages/LanguageKit/
  • https://github.com/CodaFi/IDEKit (INCREÍBLE trabajo. Hay un montón de buenas ideas en este código. Mi problema es que no tengo absolutamente ninguna idea de cómo repara ranges, o incluso si lo hace)
  • http://www.crimsoneditor.com/ (Un viejo editor de código de Windows. Tiene un tokenizador muy bueno, aunque un poco difícil de leer. No utiliza expresiones regulares. Dicho esto, basé mi lógica de tokens de este código y es MUCHO más rápido que cualquiera de los frameworks mencionados anteriormente)

Recursos:

  • http://cocoafactory.com/blog/2012/10/29/how-to-use-custom-nsattributedstring-attributes/
  • https://github.com/objcio/issue-5-textkit
  • http://alexgorbatchev.com/SyntaxHighlighter/
  • http://docs.xamarin.com/samples/TextKitDemo/ (demostración de Apple)
  • http://cocoadev.com/ImplementSyntaxHighlighting (Asegúrese de leer todos los artículos secundarios. Cosas geniales)

La mayoría de estos frameworks son los mismos. No tienen en count la conmutación de context (o tiene que escribir el contenedor para proporcionar context) para los ranges o no reparan los ranges de context a medida que un usuario modifica el text (como cadenas, comentarios multilínea, etc.). El último requisito es MUY importante. Porque si un tokenizador no puede determinar qué ranges se ven afectados por un cambio, terminará teniendo que analizar y atribuir nuevamente la cadena completa. La única exception a esto es el editor carmesí. El problema con este tokenizador es que no guarda el estado en el momento de tokenizar. En el momento del sorteo, el algorithm usa los tokens para determinar el estado del dibujo. Comienza desde la parte superior del documento, hasta que llega al range visible de text. Huelga decir que lo he optimizado al almacenar en caching el estado del documento en ciertas partes del documento.

El otro problema es que los frameworks no siguen el mismo patrón MVC que Apple hace, lo que es de esperar. Los frameworks que tienen un editor de trabajo completo usan todos los ganchos, proporcionados por la API en la que se basan (es decir, GTK, Windows, etc.), que les proporciona información sobre dónde y cuándo dibujar a qué parte de la pantalla. En mi caso, TextKit parece requerir que atribuyas todo el range cambiado en Process editing.

Tal vez mis observaciones son incorrectas. (¡Espero que lo sean!) Tal vez, ParseKit, por ejemplo, funcionará para lo que necesito y simplemente no entiendo cómo usarlo. ¡Si es así, por favor hágamelo saber! Y gracias de nuevo!

Me lo imaginé. No utilicé ninguna de las sugerencias anteriores. Dicho esto, el performance que estoy recibiendo ahora es simplemente increíble. Tenga en count que YMMV. La forma en que tokenize y cache los metadatos acerca de su cadena puede ser diferente que yo. Sin embargo, pude escribir un file PHP de 1400 líneas y solo tomó 0.015 segundos para que se complete un cambio. Simplemente increíble

Aquí está el enfoque que tomé:

Mi UIViewController es un delegado a UITextViewDelegate y UIScrollViewDelegate.

Cuando UITextViewDelegate.textViewDidChange: se llama, determino qué range de text está actualmente visible para el usuario final. Lo hice utilizando mi ITUextView subclass existente y agregando este método:

- (NSRange)visibleRangeOfText { CGRect bounds = self.bounds; UITextPosition *start = [self characterRangeAtPoint:bounds.origin].start; UITextPosition *end = [self characterRangeAtPoint:CGPointMake(CGRectGetMaxX(bounds), CGRectGetMaxY(bounds))].end; return NSMakeRange([self offsetFromPosition:self.beginningOfDocument toPosition:start], [self offsetFromPosition:start toPosition:end]); } 

Después de eso, paso el range a un object subclass NSTextStorage, donde luego realizará la magia para determinar qué líneas deben resaltarse.

Lo mismo ocurre con las llamadas al método UIScollViewDelegate. Dependiendo de qué parte de la vista se esté viendo, paso el range visible a mi llamada subclass NSTextStorage y determina si las líneas ya se han atribuido, etc.

Me doy count de que estoy dejando mucho al lector. Terminé usando lo que tenía actualmente y lo pellizqué un poco para trabajar con la implementación anterior.

Quería compartir algunos de mis descubrimientos que encontré interesantes al implementar esto:

1) Si intenta resaltar cualquier text ARRIBA sobre la línea actual, donde el cursor está en reposo, puede ver que el cursor "salta" hacia arriba dentro de la vista y luego vuelve a la position original. Estoy casi seguro de que esto es causado por la llamada al método NSTextStorage.processEditing. Pude llegar a donde el sistema solo resalta la línea que se modificó … por lo que este problema desaparece ahora.

2) Originalmente, hice esto para evitar que el cursor salte:

 NSRange selectedRange = [textView selectedTextRange]; [textView setScrollEnabled:NO]; NSRange visibleRange = [textView visibleRangeOfText]; [textStorage applyAttributesToRange:visibleRange]; [textView setScrollEnabled:YES]; 

Funcionó … pero la llamada [textView setScrollEnabled: NO] hizo un golpe MASIVO al performance. Tomó casi 3/4 de segundo para ese command solo para terminar en un file de línea 1400. No estoy seguro de qué causa que sea lento, pero pensé que valía la pena mencionar.