Mantén fluido el dibujo rápido de rectángulos separando la vista previa del arrastre activo de las formas confirmadas en el lienzo. En una app de dibujo para Mac, deja que una única superficie de gestos gestione la entrada, mantén la geometría temporal en el ámbito local y añádela a la colección de formas solo cuando el arrastre finalice correctamente.
Esta guía está dirigida a quien desarrolla una herramienta sencilla para dibujar rectángulos con clic y arrastre en su propia app para macOS. Todas las formas de ejemplo se mantienen en memoria. El objetivo es evitar que el movimiento del puntero modifique repetidamente todo el modelo de dibujo y, después, usar el análisis de rendimiento para identificar cualquier coste de renderizado que persista.
¿Por qué se ralentiza el dibujo a medida que se acumulan las formas confirmadas?
Empieza por distinguir tres operaciones: recibir la entrada, actualizar el estado y renderizar la geometría. Un arrastre puede cambiar la geometría temporal con frecuencia. Si esos cambios también reemplazan la colección confirmada, reconstruyen cada ruta o invalidan una jerarquía amplia de vistas, el trabajo asociado a cada movimiento puede aumentar a medida que crece el dibujo.
Apple explica que SwiftUI registra las dependencias y actualiza las vistas cuando estas cambian. Tanto las actualizaciones lentas de vistas individuales como las numerosas actualizaciones innecesarias pueden hacer que no se cumplan los plazos de cada fotograma. Separar el estado de la vista previa, que cambia con frecuencia, del contenido confirmado es una aplicación arquitectónica de ese modelo documentado. Optimiza el rendimiento de SwiftUI con Instruments
Canvas permite dibujar de forma inmediata mediante un contexto gráfico. Apple lo señala como una posible mejora de rendimiento para dibujos dinámicos complejos, pero no ofrece interacción ni accesibilidad para cada elemento dibujado. Asocia la gestión de entrada a una superficie de dibujo definida expresamente; si más adelante la app necesita formas que se puedan operar de manera individual, implementa por separado la selección y la accesibilidad. Canvas
El límite útil, por tanto, es explícito: la superficie de entrada gestiona el gesto, el estado temporal gestiona la vista previa y la vista principal gestiona los rectángulos confirmados. Dividir estas responsabilidades reduce las dependencias evitables. No garantiza que el sistema nunca vuelva a dibujar el lienzo confirmado.
Separa la vista previa del dibujo confirmado
Usa este plan de gestión antes de elegir optimizaciones de renderizado:
| Responsabilidad | Propietario | Cuándo cambia |
|---|---|---|
| Rectángulo del arrastre actual | Superficie de gestos | Durante el arrastre activo |
| Colección de rectángulos confirmados | Vista de dibujo principal | Una vez por cada arrastre completado correctamente |
| Dibujo de los rectángulos confirmados | Lienzo de fondo | Cuando sus entradas requieren una actualización |
| Detección del puntero | Superficie de gestos | Durante toda la secuencia del gesto |
Para la geometría temporal, @GestureState resulta útil porque SwiftUI la restablece cuando el gesto deja de estar activo. Confirma el resultado mediante onEnded, cuya función de retorno recibe el valor final del gesto. Apple especifica que esta función solo se ejecuta cuando el gesto termina correctamente; el estado del gesto se ocupa de la limpieza independientemente de cómo termine. GestureState, onEnded(_:)
Para los rectángulos, la geometría final solo requiere la ubicación inicial y la actual. No hace falta conservar cada ubicación intermedia del puntero. Apple documenta esas ubicaciones por separado de los puntos finales predichos; usa la ubicación final real al confirmar el rectángulo. DragGesture.Value
Crea una herramienta completa de rectángulos en memoria
Los dos bloques de código siguientes forman un único archivo fuente de Swift completo. En un proyecto de app SwiftUI para macOS cuyo destino sea macOS 12 o posterior, reemplaza el archivo de entrada de la app generado por ambos bloques y elimina cualquier otra declaración de punto de entrada de la app. Canvas está disponible desde macOS 12. Este ejemplo se revisó, pero no se pudo compilar aquí porque el entorno no dispone de una cadena de herramientas de Swift. Canvas
El primer bloque define la app, la colección confirmada y su renderizador. La vista principal recibe solo los rectángulos completados; no lee el estado del arrastre activo.
import SwiftUI
@main
struct RectangleDrawingApp: App {
var body: some Scene {
WindowGroup {
DrawingBoard()
}
}
}
struct DrawingBoard: View {
@State private var rectangles: [CGRect] = []
var body: some View {
VStack(spacing: 8) {
Text("Drag to draw a rectangle")
Text("Committed rectangles: \(rectangles.count)")
ZStack {
Color.white
CommittedRectangles(rectangles: rectangles)
.allowsHitTesting(false)
RectangleGestureSurface { rectangle in
rectangles.append(rectangle)
}
}
.clipped()
.frame(minWidth: 480, minHeight: 320)
}
.padding()
}
}
struct CommittedRectangles: View {
let rectangles: [CGRect]
var body: some View {
Canvas { context, _ in
for rectangle in rectangles {
context.stroke(
Path(rectangle),
with: .color(.blue),
lineWidth: 2
)
}
}
}
}
Añade el siguiente bloque al mismo archivo. La superficie transparente cubre el área de dibujo, define una forma rectangular explícita para las pruebas de impacto y es la única propietaria del gesto de dibujo. Apple documenta que contentShape define la forma usada para las pruebas de impacto y que allowsHitTesting(false) excluye una vista de dichas pruebas. contentShape(_:eoFill:), allowsHitTesting(_:)
struct RectangleGestureSurface: View {
let commit: (CGRect) -> Void
@GestureState private var preview: CGRect? = nil
var body: some View {
ZStack {
Color.clear
Canvas { context, _ in
if let rectangle = preview {
context.stroke(
Path(rectangle),
with: .color(.orange),
lineWidth: 2
)
}
}
.allowsHitTesting(false)
}
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.updating($preview) { value, state, _ in
state = Self.rectangle(
from: value.startLocation,
to: value.location
)
}
.onEnded { value in
let rectangle = Self.rectangle(
from: value.startLocation,
to: value.location
)
commit(rectangle)
}
)
}
private static func rectangle(
from start: CGPoint,
to end: CGPoint
) -> CGRect {
CGRect(
x: min(start.x, end.x),
y: min(start.y, end.y),
width: abs(end.x - start.x),
height: abs(end.y - start.y)
)
}
}
minimumDistance: 0 elimina el requisito de desplazamiento mínimo para reconocer el gesto. El espacio de coordenadas local mantiene las posiciones del gesto alineadas con el lienzo de vista previa, que tiene el mismo tamaño. Estas son opciones de configuración documentadas para gestos, no una garantía sobre la frecuencia de los eventos ni la latencia de pantalla. DragGesture
La función auxiliar del rectángulo normaliza las cuatro direcciones de arrastre. Como ejemplo, arrastrar desde (90, 80) hasta (30, 20) produce un origen de (30, 20) y un tamaño de (60, 60). La vista previa y la confirmación usan el mismo cálculo, así que al soltar el puntero no cambia la fórmula de la geometría.
Esta herramienta mínima también confirma los rectángulos de ancho o alto cero. Por tanto, un clic puede incrementar el recuento sin producir un rectángulo visible. Si el producto requiere un tamaño mínimo, añade esa regla explícitamente al confirmar y ofrece al usuario la información adecuada. El ejemplo conserva cada gesto completado correctamente, en vez de descartar silenciosamente los resultados pequeños.
El área visible está recortada, pero la geometría almacenada permanece intacta. Por tanto, un rectángulo que se extienda fuera del tablero puede contener coordenadas que queden fuera de este. Antes de añadir límites a las coordenadas, decide si un cambio de tamaño futuro debería revelar esa geometría.
Diagnostica el coste restante antes de añadir una caché
El ejemplo elimina las mutaciones del modelo confirmado de la ruta de actualización del arrastre. Aun así, su renderizador de contenido confirmado recorre la colección cada vez que se ejecuta. Distingue entre reducir la propagación del estado y reducir el trabajo de renderizado: que una vista esté separada no significa que exista una caché de imágenes persistente documentada.
Usa una comparación controlada para averiguar qué coste persiste. Elige cantidades ilustrativas de formas en memoria, como 0, 500 y 5.000 rectángulos. Mantén constantes las dimensiones de los rectángulos, el tamaño de la ventana, la escala de pantalla y la configuración de compilación. Estas cantidades son datos de prueba, no límites de capacidad ni resultados medidos.
Registra por separado tres intervalos: el arrastre mientras la colección permanece sin cambios, la liberación al confirmar y el cambio de tamaño de la ventana. Una ralentización continua durante el arrastre sugiere un trabajo distinto del que causa una pausa solo al confirmar. El comportamiento al cambiar el tamaño puede revelar la reconstrucción de geometría o cachés que no se produce durante el dibujo normal.
La sesión de Apple sobre análisis de rendimiento de SwiftUI describe las herramientas Long View Body Updates, Long Representable Updates y Cause & Effect Graph, además de Time Profiler. Usa el grafo de dependencias para averiguar por qué se actualizan las vistas confirmadas durante un arrastre; usa las muestras de CPU para detectar la construcción costosa de geometría o el trabajo de renderizado. Apple presentó ese flujo de trabajo con Instruments 26. Optimiza el rendimiento de SwiftUI con Instruments
Para las interrupciones más amplias, la sesión WWDC26 de Apple sobre la capacidad de respuesta empieza con Time Profiler y distingue entre la ejecución que mantiene la CPU ocupada y el tiempo de espera. Aplica esa distinción antes de trasladar el trabajo entre ejecutores o reemplazar la pila de renderizado. Analiza, corrige y verifica: mejora la capacidad de respuesta de las apps con Instruments
La siguiente guía de decisiones es una inferencia de ingeniería basada en esas herramientas de análisis y en el diseño de gestión descrito anteriormente:
| Comportamiento observado | Qué investigar primero | Cambio posible |
|---|---|---|
| El estado de dibujo principal cambia durante el movimiento del puntero | Las funciones de retorno de actualización del gesto y el estado compartido | Mantener el estado de la vista previa dentro de la superficie de gestos |
| El dibujo confirmado domina las muestras durante el movimiento | El recorrido repetido y la creación de rutas | Precocinar la geometría o introducir una caché para el contenido confirmado |
| Las pausas al soltar se alargan a medida que crece la colección | La mutación del array y la reconstrucción completa del dibujo | Reducir las instantáneas conservadas y reconstruir solo el contenido derivado afectado |
| El dibujo de rectángulos es fluido, pero los trazos libres largos son lentos | El tamaño de la ruta activa y su reconstrucción | Analizar por separado el trazo activo que va creciendo |
| Las pausas se producen principalmente al cambiar el tamaño | El renderizado dependiente del tamaño y la invalidación de la caché | Definir explícitamente cómo se comporta el contenido derivado al cambiar el tamaño |
Una caché añade otro límite de gestión: la geometría de los rectángulos sigue siendo la fuente de verdad y la caché es contenido derivado reemplazable. Incluye el tamaño del dibujo, la escala de pantalla y los estilos pertinentes en el diseño de invalidación. Para un dibujo al que solo se añaden elementos, las actualizaciones incrementales de la caché son una posible optimización; las ediciones y eliminaciones requieren una estrategia clara de reconstrucción o invalidación por regiones. Verifica el resultado con las mismas cantidades de entrada antes de conservar la complejidad añadida.
Comprueba si se copian arrays al confirmar
Los arrays de Swift tienen semántica de valor y usan almacenamiento con copia al escribir. Las copias pueden compartir almacenamiento hasta que una mutación requiere almacenamiento independiente; los arrays que contienen instancias de clases siguen compartiendo referencias a esas instancias. No se debe afirmar que pasar un array a una vista o añadir un elemento a un array siempre copia todos sus elementos. Array
En esta herramienta, evita guardar una nueva instantánea completa del dibujo en cada actualización del puntero. Si el análisis detecta copias al confirmar, examina qué instantáneas siguen activas y cómo se comparte el modelo de dibujo. Sustituir tipos de valor por objetos mutables compartidos solo para evitar copias cambia la semántica de propiedad y requiere una decisión de diseño aparte.
Considera la presentación asíncrona como una opción que debe medirse
Apple define rendersAsynchronously como una opción que permite al lienzo presentar su contenido a la vista principal de forma asíncrona. Esa descripción no promete una entrega más rápida de los gestos, no traslada el controlador de gestos a un ejecutor en segundo plano ni elimina el recorrido de la colección. Evalúala por separado después de establecer los límites de entrada y estado. rendersAsynchronously
¿Cuándo debería AppKit gestionar la superficie de dibujo?
Usa una superficie de AppKit personalizada cuando la tarea requiera gestionar directamente los métodos del responder o invalidar explícitamente regiones de dibujo. Apple documenta NSView como la infraestructura para dibujar y gestionar eventos, y admite su creación como subclase para personalizar su comportamiento. Es una alternativa compatible, con sus propias responsabilidades de renderizado. NSView
En una implementación de AppKit, conserva la misma secuencia: inicializa la geometría temporal en mouseDown(with:), actualízala en mouseDragged(with:) y confírmala en mouseUp(with:). Convierte event.locationInWindow a las coordenadas de la vista receptora antes de calcular la geometría. La guía archivada de Apple sobre eventos del ratón documenta esta secuencia y la conversión de coordenadas; la referencia actual de NSView documenta los métodos de responder compatibles. Gestión de eventos del ratón, NSView
Solicita el redibujado estableciendo needsDisplay = true; Apple especifica que la vista se vuelve a dibujar en el siguiente ciclo del bucle de eventos. En un diseño basado en regiones, invalida la unión de los límites anteriores y nuevos de la vista previa, ampliada para incluir el trazo. La invalidación por regiones solo reduce el recorrido si la implementación del dibujo también evita procesar el contenido que no se ha modificado. needsDisplay, Gestión de eventos del ratón
Incorpora la superficie con NSViewRepresentable: créala en makeNSView, sincroniza el estado externo en updateNSView y usa un coordinador para comunicar los cambios a SwiftUI. Actualiza el destino de las funciones de retorno durante las actualizaciones y evita restablecer un borrador activo solo porque SwiftUI proporcione otro valor representable. Apple también indica que SwiftUI controla el diseño de la vista representada; no sobrescribas directamente su marco ni sus límites administrados. NSViewRepresentable
Elige un único responsable de la entrada de dibujo. Si AppKit gestiona la secuencia de dibujo, no confirmes también esa secuencia mediante un gesto de arrastre de SwiftUI superpuesto. Para una subclase directa de NSView, Apple indica que las implementaciones que gestionan los eventos de ratón enumerados no deben llamar a la implementación de la superclase para esos eventos gestionados. NSView
Mantén la primera mejora pequeña y observable: estado de vista previa local, un único límite de confirmación y un renderizador de contenido confirmado cuyas entradas no cambien durante el movimiento. Después, compara dibujos sintéticos de distintos tamaños en Instruments. El resultado del análisis te dirá si el siguiente cambio útil corresponde a la gestión de dependencias, la propiedad de la colección, la preparación de geometría o el renderizado.
Fuentes
Canvas — Documentación para desarrolladores de Apple.
GestureState — Documentación para desarrolladores de Apple.
onEnded(_:) — Documentación para desarrolladores de Apple.
DragGesture — Documentación para desarrolladores de Apple.
DragGesture.Value — Documentación para desarrolladores de Apple.
contentShape(_:eoFill:) — Documentación para desarrolladores de Apple.
allowsHitTesting(_:) — Documentación para desarrolladores de Apple.
Array — Documentación para desarrolladores de Apple.
rendersAsynchronously — Documentación para desarrolladores de Apple.
Optimiza el rendimiento de SwiftUI con Instruments — Apple Developer.
Analiza, corrige y verifica: mejora la capacidad de respuesta de las apps con Instruments — Apple Developer.
NSView — Documentación para desarrolladores de Apple.
needsDisplay — Documentación para desarrolladores de Apple.
NSViewRepresentable — Documentación para desarrolladores de Apple.
Gestión de eventos del ratón — Archivo de documentación de Apple.