La estructura técnica interna del editor de texto de Apple
(papereditor.app)- Paper funciona en las plataformas de Apple como un editor
TextViewbasado en TextKit 1, dondeNSTextStorage,NSTextContainer,NSLayoutManager,TextViewyScrollViewconstruyen en conjunto la experiencia de edición NSAttributedStringrepresenta texto enriquecido mediante una cadena y atributos (attributes) por rango, y Paper separa los metaatributos para la estructura Markdown de los atributos de estilo para la visualización en pantalla- La actualización de estilos varía según si se abre un documento, cambia el texto o cambia la configuración; los atributos que afectan el layout se agrupan en transacciones, y los atributos decorativos se actualizan sin recalcular el layout
- El rendimiento de entrada mejora evitando volver a parsear párrafos completos según si se edita alrededor de caracteres especiales de Markdown, pero las estructuras que pueden afectar el estilo de todo el documento, como los bloques de código, requieren manejo de excepciones
- Copiar y pegar funciona sobre varias representaciones de datos en UTI y pasteboard; en formatos como Markdown, las UTI semipúblicas no definidas por Apple pueden generar inconsistencias de prioridad entre apps, conversiones a RTF y problemas de UX
Stack de edición TextView de Apple
- Paper está construido sobre el antiguo framework TextKit 1, y los mismos conceptos y abstracciones existen tal cual en TextKit 2 o bajo API mejores
NSTextViewyUITextViewtienen diferencias, pero sus API son lo bastante similares como para tratarlos como una familiaTextViewTextViewes un componente grande que se vuelve más complejo con cada versión del sistema operativo, y la app TextEdit está compuesta casi por un únicoTextView-
Capas centrales que forman el editor
NSTextStorage- Almacena la cadena de texto original
- Almacena los atributos de pares cadena-valor asociados a rangos de texto
- Emite eventos de cambios en el texto y en los atributos
NSTextContainer- Define la forma y el tamaño del área donde entran los glifos
- En la mayoría de los casos es rectangular, pero también puede tener formas arbitrarias
NSLayoutManager- Observa los rangos de atributos de
NSTextStoragey calcula el tamaño y espaciado de los glifos - Extrae glifos vectoriales de la fuente y convierte un carácter en uno o más glifos
- Calcula el inicio y el final de cada línea, la cantidad de líneas y la altura total del texto
- Observa los rangos de atributos de
TextView- Dibuja el layout de glifos creado por
NSLayoutManager - Sincroniza la altura de la vista con la altura actual del layout de texto
- Gestiona entrada, selección, cursor, atributos de escritura,
textContainerInset, dictado, copiar y pegar, corrección ortográfica, etc.
- Dibuja el layout de glifos creado por
ScrollView- Gestiona la parte visible de
TextView, el desplazamiento, las barras de desplazamiento y el zoom - Puede tener
contentInsetaparte deltextContainerInsetdeTextView
- Gestiona la parte visible de
-
Diferencias de desplazamiento entre AppKit y UIKit
- En AppKit,
NSScrollViewcontiene unNSClipViewy dosNSScroller, y elNSClipViewcontiene elNSTextView, por lo que varias clases construyen juntas el comportamiento de desplazamiento - En UIKit,
UITextViewhereda deUIScrollView, así que incluye directamente la lógica de desplazamiento - Si el cursor se mueve fuera del área visible limitada por
contentInset,UITextViewse desplaza automáticamente - En un editor de iOS, cuando el cursor queda detrás del teclado y se desplaza a la línea siguiente, es porque el
contentInsetinferior se configura dinámicamente con la altura actual del teclado
- En AppKit,
NSAttributedString y consulta de atributos
NSAttributedStringes la base de la edición de texto enriquecido en los frameworks de Apple y está compuesto por dos elementos- Una cadena de texto plano
- Pares de atributos cadena-valor asociados a rangos de texto dentro de la cadena
- Los atributos se usan principalmente para styling, pero el desarrollador también puede adjuntar cualquier par cadena-valor que necesite
NSRangese compone delocationylength, yNSMakeRange(10, 5)significa un rango de 5 caracteres que empieza en la posición 10, es decir, las posiciones 10 a 14- Si el mismo atributo se especifica en varios rangos en la misma posición, tiene prioridad el rango aplicado más recientemente
-
Revisar atributos y rangos en una posición específica
- El valor de un atributo en una posición específica se revisa con
attribute:atIndex:effectiveRange: - Si el valor devuelto es
nil, ese atributo no existe; si hay un valor, en el ejemplo sería un valor de atributo como un objetoNSFontoUIFont - Si se pasa un puntero
NSRangecomo último argumento, se puede recibir el rango en el que continúa el mismo valor de atributo o un rango vacío sin atributo - Según la documentación,
effectiveRangeno necesariamente es el rango máximo y depende de la implementación - Si se necesita el rango máximo garantizado, hay que usar
attribute:atIndex:longestEffectiveRange:inRange: - También existe un método para obtener todas las combinaciones de atributos en una posición específica y el rango en el que continúa esa combinación
- Para recorrer atributos dentro de un rango se puede usar
enumerateAttribute:inRange:options:usingBlock:
- El valor de un atributo en una posición específica se revisa con
Pipeline de styling Markdown de Paper
- El styling de Paper se basa en aplicar atributos especiales definidos por el framework a rangos de texto
- Antes del styling, también usa metaatributos personalizados para identificar la estructura del texto
-
Metaatributos y atributos de estilo
- Metaatributos
- Los define el parser de Markdown para identificar partes individuales de la sintaxis Markdown
- Son pares cadena-valor personalizados puramente semánticos
- No afectan la apariencia visual del texto
- Pueden verse como un AST simplificado para Markdown
- Atributos de estilo
- Son atributos visuales aplicados sobre las partes marcadas con metaatributos
- Son pares cadena-valor integrados definidos por AppKit y UIKit
- Metaatributos
-
Eventos que disparan la actualización de atributos
- Los atributos se sincronizan con el texto Markdown de
NSTextStoragey con los cambios de configuración que afectan el texto - Al abrir un documento, se actualizan por completo los metaatributos y los atributos de estilo
- Ante un cambio de texto, se actualizan parcialmente los metaatributos y los atributos de estilo de las partes afectadas
- Ante un cambio de configuración, no se modifican los metaatributos y solo se actualizan por completo los atributos de estilo
- Los atributos se sincronizan con el texto Markdown de
-
Orden de actualización y costo de layout
- Primero se inicia una transacción de edición de texto para agrupar los cambios de atributos
- Si se modifican atributos sin una transacción, se dispara cada vez el costoso recálculo de layout de
NSLayoutManager - Se parsea la cadena Markdown y se divide en fragmentos marcados con metaatributos; cuando cambia la configuración, este paso se omite porque la estructura Markdown no cambia
- Primero se actualizan los atributos visuales que pueden afectar la posición o el tamaño de los glifos, y
NSParagraphStylese trata como el atributo más importante porque afecta el layout de líneas y párrafos - Después de terminar la transacción, se actualizan los atributos decorativos que no afectan el layout
- Los atributos decorativos corresponden a los rendering attributes en la terminología de Apple, y como existen en
NSLayoutManagery no enNSTextStorage, no reconocen transacciones
-
Atributos de escritura
- Los atributos de escritura (typing attributes) se vinculan, cuando la selección está vacía, con los atributos de la posición anterior al cursor; y cuando la selección no está vacía, con los atributos del punto inicial de la selección
- Al escribir un carácter, los atributos de escritura se aplican automáticamente al texto recién insertado
- En un editor Markdown son menos importantes porque el estilo deriva de la sintaxis Markdown
- En un editor de texto enriquecido son más importantes porque el estilo queda asociado al cursor hasta que se desactiva o se mueve el cursor a otra posición
- El Preview Mode de Paper ofrece, incluso dentro de un editor Markdown, una experiencia de edición de texto enriquecido donde se destacan atributos de escritura alternables
Rendimiento de entrada y uso de metaatributos
- Separar metaatributos, layout y atributos decorativos ayuda a procesar rápidamente algunos cambios del editor
- Cambiar entre modo claro y oscuro solo actualiza atributos decorativos, por lo que no dispara layout y es muy rápido
- Un cambio de configuración como ajustar el tamaño del texto requiere recalcular el layout de todo el documento, pero aun así es más rápido que volver a parsear toda la estructura Markdown
-
Optimización de entrada Markdown
- El factor de rendimiento más importante en un editor de texto es la velocidad de entrada
- Por la naturaleza de Markdown, un solo cambio de texto puede afectar el estilo de un párrafo completo
- Volver a parsear y estilizar todo el párrafo en cada pulsación de tecla es técnicamente lo más correcto, pero en párrafos largos puede volver más lenta la edición
- Paper usa un algoritmo que decide qué hacer observando el siguiente carácter que se ingresa y los caracteres cercanos
- Si se ingresa un símbolo especial de Markdown o hay símbolos especiales alrededor de la posición de edición, actualiza todo el párrafo
- De lo contrario, se apoya en los atributos de escritura para acelerar el editor en la mayoría de las situaciones de entrada
-
Excepción de bloques de código y caché de objetos de valor
- Los bloques de código son la única estructura Markdown multipárrafo en Paper
- En un documento con bloques de código, una sola pulsación de tecla puede afectar el estilo de todo el documento
- En documentos que superan cierta cantidad de caracteres, Paper ignora el manejo de bloques de código para mantener un editor rápido para la mayoría de usuarios a quienes no les interesa el código
- Esta decisión también vuelve a Paper más útil para usuarios cercanos al mundo del desarrollo
- Los objetos complejos usados como valores de atributos se vuelven a asignar en cada pulsación de tecla, pero como no cambian mientras no cambie la configuración que afecta al texto, se almacenan en caché y se reutilizan
NSFont/UIFontNSColor/UIColorNSParagraphStyle
-
Funciones que usan metaatributos
- Atajos de formato
- Para poder alternar un estilo, se necesita conocer la información de estilo existente del texto Markdown seleccionado
- Si la selección envuelve por completo el mismo estilo, ese estilo se elimina
- Si la selección no tiene el mismo estilo, se agrega el estilo
- Si la selección envuelve solo una parte del mismo estilo, el estilo se mueve a la selección
- Los estilos en conflicto que definen un tipo de párrafo, como heading y blockquote, no pueden mezclarse, por lo que deben eliminarse antes de agregar un nuevo estilo
- Navegación de capítulos
- Al moverse al límite del capítulo anterior o siguiente, usa metaatributos para encontrar el heading en función de la posición del cursor
- Outline
- Recorre todos los headings y, al tocar un elemento del outline, el cursor se mueve a ese capítulo
- Reordenamiento de capítulos
- Permite reorganizar capítulos desde el outline
- Conversión de formato
- Para convertir Markdown a RTF, HTML o DOCX, se necesita conocer la estructura del texto
- Como Paper no incluye librerías externas, recorre el modelo de texto ya parseado y genera el formato de salida
- Atajos de formato
Matemática del contenedor de texto y comportamiento de selección
-
Cálculo de longitud de línea y márgenes
- La regla central del contenedor de texto es mantener la longitud de línea preferida y dividir el espacio sobrante entre los inset de ambos lados
- También hay situaciones en las que hay que fingir simetría, como cuando las etiquetas heading se colocan fuera del flujo normal de texto
- En ese caso, el contenedor de texto se mueve hacia la izquierda y el párrafo se indenta con
NSParagraphStyle - Si hay espacio suficiente, los márgenes se mantienen visualmente cerca de la simetría
- Si no hay espacio adicional, se rompe la simetría para mantener la longitud de línea especificada, pero solo mientras quede padding derecho
- Si no queda padding, el margen mínimo tiene prioridad sobre el ancho de línea preferido
- Con una combinación de
minymaxse pueden implementar márgenes que se repliegan progresivamente
-
Ancla de selección
- La selección de texto siempre tiene un punto de ancla
- En Mac, al hacer clic y arrastrar, el punto donde se hizo clic se vuelve la referencia; al pasar por ese punto, la dirección en la que la selección crece o se reduce se invierte
- En iOS, al arrastrar uno de los manejadores de selección, el lado opuesto se convierte en el ancla, y viceversa
- La misma lógica se aplica al extender una selección con el teclado
- Con
Option+flecha izquierda/derecha se mueve entre límites de palabras, y si además se mantieneShift, se selecciona por palabra - Incluso si después de seleccionar con clic y arrastre se sigue extendiendo o reduciendo con el teclado, el punto inicial del clic permanece como ancla
-
Affinity de selección
- La affinity de selección determina si, en un límite de salto de línea, el punto de inserción aparece después del último carácter de la línea anterior o antes del primer carácter de la línea siguiente
- Al mover el cursor con las flechas, en los espacios alrededor de un salto de línea simplemente se cambia de línea
- Al moverse al final de la línea con un atajo, el cursor puede permanecer en la misma línea y quedar pegado a la derecha del espacio del salto de línea
TextViewrealiza comportamientos similares de affinity de selección también en otras situaciones
-
Undo coalescing
- Undo coalescing es el algoritmo que decide cuándo agrupar el texto ingresado en una única acción que se puede deshacer
TextViewparece usar un algoritmo simple que agrupa el contenido escrito siempre que no sea interrumpido por otra acción- Aunque se haga una pausa y luego se siga escribiendo, queda agrupado en un único undo, y los saltos de línea tampoco cortan el coalescing
- Sublime prefiere agrupar palabras separadas como unidades de undo durante la escritura, y al escribir palabras largas a veces las divide en unidades de undo más pequeñas
- No hay una respuesta correcta para este comportamiento, y cada editor puede tener estrategias distintas
UTI, pasteboard e intercambio de datos entre apps
-
Uniform Type Identifiers
- UTI es un sistema jerárquico en el que los tipos de datos
conform totipos de datos padre, como si los heredaran - Los tipos
public.*los define Apple e identifican formatos de uso extendido comopublic.htmlypublic.jpeg - Para evitar colisiones, los desarrolladores pueden crear sus propios identificadores con un esquema de nombre de dominio inverso
- La ventaja del sistema jerárquico es que, si una app puede ver todos los formatos de texto, puede declarar solo
public.textsin enumerar cada formato individual - Paper declara que puede abrir todos los archivos de texto y, aunque no tenga highlighting, puede abrir formatos de texto como
.htmly.rtf - Como los archivos son un concepto multiplataforma, en la práctica las extensiones de archivo se usan como identificadores
- Por compatibilidad, todas las UTI pueden definir una o más extensiones de archivo asociadas
- Markdown es un formato muy usado, pero como Apple no lo define, aparecen casos complicados relacionados con una UTI semipública
- UTI es un sistema jerárquico en el que los tipos de datos
-
Pasteboard y varias representaciones
- El pasteboard de Apple es un diccionario que mapea UTI a texto serializado o datos binarios
- Con Clipboard Viewer de Xcode Additional Tools se puede ver en tiempo real el contenido del pasteboard
- Una sola acción de copia escribe simultáneamente varias representaciones de los mismos datos
- Algunas apps también usan identificadores legacy que no son UTI, como
NeXT Rich Text Format v1.0 pasteboard type, por compatibilidad hacia atrás - Por lo general, el editor elige el formato más rico que puede procesar
Paste and Match StyleoPaste as Plain Texten un editor de texto enriquecido hace que se use el formato de texto plano del pasteboard- El estilo aplicado al texto pegado normalmente se toma de los atributos de escritura
- Arrastrar y soltar también funciona sobre la base de otro pasteboard
- Para copy-paste estándar se usa el general pasteboard, y para interacciones personalizadas entre apps se pueden crear pasteboards personalizados
-
RTF y
NSAttributedString- RTF es básicamente una forma serializada de
NSAttributedStringy, a la inversa,NSAttributedStringes la interfaz de programación de RTF - Como
TextViewfunciona sobreNSTextStorage, una subclase deNSAttributedString, es compatible de base con el pasteboard - Se puede copiar contenido al pasteboard sin código adicional
- RTF es básicamente una forma serializada de
-
Problemas entre apps con la UTI de Markdown
- Al copiar y pegar entre editores Markdown, pueden surgir problemas aunque ambas apps implementen el protocolo estándar
- Si el primer editor exporta Markdown como
public.texty la representación de texto enriquecido comopublic.rtf, el segundo editor puede no saber que se trata de Markdown nativo y elegirpublic.rtf - En ese caso ocurre una doble conversión innecesaria y, como cada app convierte Markdown↔RTF de forma distinta, pueden aparecer pequeños problemas de formato, como saltos de línea adicionales
- También pueden hacerse visibles diferencias fundamentales de estilo entre Markdown y RTF
- Para que funcione de forma natural, ambas apps tendrían que exportar la UTI
net.daringfireball.markdowny darle prioridad sobrepublic.rtf - Paper intentó exportar la UTI de Markdown, pero otras apps parecían no darle prioridad frente al texto enriquecido
- Pages sí prioriza
net.daringfireball.markdownpor encima depublic.rtf, pero en vez de convertirlo a texto enriquecido, parece insertar tal cual la cadena Markdown original - Por eso Paper eliminó la UTI de Markdown
-
Por qué Paper ofrece RTF y cómo funciona compartir en iOS
- Paper ofrece RTF para que copiar y pegar en editores de texto enriquecido se sienta natural
- Como buen ciudadano del sistema operativo, debe ofrecer varios formatos que representen los datos copiados para que la app receptora elija el formato más rico que pueda procesar
- Si se copia texto Markdown desde Paper y se pega en Mail, se pega como texto enriquecido con buen formato, no como una variante de Markdown
- La función Compartir de iOS es similar a copy-paste, pero con una UI agregada
- En iOS Compartir, la app exporta datos en varios formatos y la app receptora elige el formato que quiere
- A diferencia del pasteboard, para aparecer en la lista superior de apps de Compartir en iOS, una app debe ofrecer explícitamente una share extension con UI personalizada
1 comentarios
Comentarios de Hacker News
Está realmente excelente. Creo que de ahora en adelante, para material introductorio de TextKit, voy a mostrar primero este artículo en lugar de https://www.objc.io/issues/5-ios7/getting-to-know-textkit/
Es el nuevo motor de layout que hoy se usa por defecto en los campos de texto de iOS y macOS, y probablemente en el futuro será la única opción
La app TextEdit está compuesta casi por completo por un solo TextView
Su equivalente del lado de Windows sería WordPad, basado en el control RichEdit. Otra cosa interesante es que RTF es, en la práctica, un formato de serialización de NSAttributedString
Lo mismo pasa con el control RichEdit de Windows, y de hecho parece que la implementación de Windows salió primero: https://en.wikipedia.org/wiki/Rich_Text_Format
Me confundió un poco la parte de procesar atributos decorativos fuera de la transacción de edición. Decía: “como están en NSLayoutManager y no en NSTextStorage, ni siquiera conocen la transacción”, pero los atributos decorativos como el color normalmente van en NSTextStorage
¿El autor quiere decir que el color aplicado a los caracteres de Markdown se maneja con el soporte de atributos temporales de NSLayoutManager? Normalmente esa función se usa para colorear palabras con errores ortográficos, así que me pregunto cuál es el objetivo
La ventaja de usar atributos temporales es que los cambios son mucho más rápidos que invocar la transacción completa de NSTextStorage. Por ejemplo, en Paper el cambio entre modo claro y oscuro es muy rápido
Es un artículo realmente bueno y, personalmente, me cayó justo a tiempo porque ahora mismo también estoy trabajando con NSTextView
Me da curiosidad cómo consiguió la información necesaria para escribir algo así. ¿Código de otras personas, experiencia dolorosa, developer.apple.com?
Incluso en la era de los documentos DOM, viendo Notion o GitBook, cuando haces cosas casi mágicas con parsing y manipulación de texto, a menudo terminas volviendo a las cadenas con atributos
Es una estructura realmente elegante, y no entiendo por qué no es más conocida. El artículo también es increíblemente bueno
Como alguien que alguna vez intentó crear un editor de texto completamente desde cero, siento que un artículo así me habría servido muchísimo como recurso
Uso esta app y me gusta mucho. Reemplazó a todas mis demás apps de Markdown, incluyendo Obsidian e iA Writer
Ojalá hubiera documentación así para más componentes de iOS
Qué alivio ver que en 2024 todavía hay alguien haciendo Cocoa
Como alguien que ha desarrollado apps Android durante mucho tiempo, me pareció interesante que Apple aborde las cosas de una forma un poco distinta y más cuidadosa
En Android, básicamente todo lo relacionado con layout y renderizado lo maneja la clase Layout y sus subclases, y TextView implementa parte de la lógica de edición y selección. La única diferencia entre EditText y TextView es que EditText deja activadas funciones de edición que ya vienen dentro de TextView
El problema de este enfoque algo monolítico y de su API limitada es que te bloquea cuando quieres controlar con más detalle el renderizado de texto de tu app. Por ejemplo, aunque quieras acceder a glifos individuales después de que termina el layout, no se puede