El shader tearDistortion recibe, por píxel, la posición actual y una serie de parámetros que llegan desde SwiftUI:
tearStart: punto de inicio del desgarro (en el borde real de la vista).tearEnd: punto actual del dedo (proyectado sobre la línea fija del desgarro).tearProgress: progreso general del gesto (0...1).maxGap: apertura máxima entre las dos mitades.edgeRoughness: irregularidad del borde rasgado.viewSize: tamaño de la vista para recortes seguros.
Flujo interno del shader:
- Si
tearProgresses casi 0, no altera nada (layer.sample(position)). - Construye la línea del desgarro con
tearDir = tearEnd - tearStart. - Obtiene dos ejes:
tearNorm: eje a lo largo del corte.tearPerp: eje perpendicular al corte.
- Para cada píxel, calcula:
alongTear: cuánto avanza ese píxel sobre la línea de corte.acrossTear: distancia perpendicular al corte.
- Define una apertura en forma de cuña:
- máxima en
tearStart. - cero en
tearEnd.
- Aplica ruido sinusoidal sobre el borde para simular fibras rasgadas (
edgeRoughness). - Si el píxel cae dentro del hueco (
absDist < halfGap), lo vuelve transparente. - Si no cae en el hueco, desplaza el muestreo hacia su lado para “separar” visualmente las dos mitades.
- Si el muestreo queda fuera de
viewSize, devuelve transparente para evitar artefactos.
Resultado: se ve un desgarro limpio con separación de mitades y borde irregular, sin deformación tipo “curl”.
La integración está separada en:
TearState.swift: estado + cálculos geométricos.TearModifier.swift: orquestación del gesto + uniforms del shader.Extensions/View+TearKitExtensions.swift: API públicatearable(...).
Variables clave:
edgePoint: ancla real del desgarro en el borde.fingerPoint: extremo actual del desgarro (dedo proyectado).fixedDirection: dirección unitaria bloqueada al inicio del drag.directionLocked: indica si ya se bloqueó la dirección.gapWidth: apertura enviada comomaxGap.tearProgress: progreso enviado al shader.lockProjection: baseline del momento de bloqueo para evitar saltos.maxTrajectoryDistance: longitud máxima de la línea de desgarro que queda dentro de la vista.hasFullyTorn: evita reactivar el gesto cuando ya se rompió completamente.
En body(content:):
geometryReaderguardaviewSize.layerEffectcrea unShader(function:arguments:)paratearDistortiony le pasa los valores del estado.isEnabledactiva el shader solo cuando:
- hay gesto activo,
- la dirección ya está bloqueada,
- y hay apertura real (
gapWidth > 0.1).
- El modifier no aplica caída/opacidad; esas reacciones se delegan fuera del componente mediante callbacks.
- Al primer
onChanged, si elstartLocationestá cerca de un borde:
- activa el estado,
- inicializa
edgePointyfingerPointcon ese inicio.
- Mientras no hay bloqueo (
directionLocked == false):
- espera a superar ~20pt de drag,
- cuando supera, llama
lockDirection(from:to:cutDirection:).
lockDirection:
- calcula dirección unitaria,
- hace raycast hacia atrás para encontrar el borde real (
edgePoint), - guarda
lockProjectioncon la proyección actual del dedo para usarlo como baseline.
- Hasta terminar esa fase inicial, se fuerza:
tearProgress = 0,gapWidth = 0,- sin efecto visible.
- Ya bloqueada la dirección:
- proyecta el dedo con
projectedEnd(fingerLocation:), - limita la proyección al tramo válido dentro de la vista,
- calcula distancia, progreso y apertura.
- En
onEnded:
- si
tearProgress >= breakThreshold(por defecto0.9): marca rotura total y notifica por callback. - si no: recuperación animada.
La decisión se toma al finalizar el gesto:
- Se compara
tearProgresscontrabreakThreshold.tearProgressse calcula como:- distancia recorrida sobre la línea fija
- dividida por la distancia máxima de esa misma línea dentro de la vista.
- Si supera el umbral:
- se fuerza
tearProgress = 1y gap máximo, - se dispara
onCompletion(true).- La animación/acción final (caída, fade, navegación, etc.) la decide la vista que usa el modifier.
- Si no supera el umbral:
- primero se anima el cierre del corte por frames (interpolación explícita de finger/progress/gap a 0),
- y al terminar se hace
reset()interno, - se dispara
onCompletion(false).
La extensión tearable(...) ahora expone:
edgeRoughness: intensidad de irregularidad del borde rasgado.maxGapWidth: apertura máxima cerca del inicio del corte.breakThreshold: umbral de rotura total (0...1).cutDirection: modo de dirección (.vertical,.horizontal,.anyDirection).isBroken:Binding<Bool>opcional para observar/controlar estado roto.onProgressChanged: callback continuo del progreso (0...1).onCompletion: callback final con resultado booleano.
Ejemplos rápidos de cutDirection:
.vertical: restringe el corte a vertical..horizontal: restringe el corte a horizontal..anyDirection: sigue la dirección inicial del drag.
Semántica de onCompletion:
true: la vista se rompió del todo y cae.false: no llegó al umbral y se recuperó.
Semántica de isBroken:
- poner
falsedispara recuperación/reset. - poner
truefuerza el estado roto externamente.
- Usuario arrastra desde el borde.
- SwiftUI actualiza
TearState. TearModifiermanda uniforms al shader.tearDistortioncalcula hueco + separación por píxel.- Al soltar, se evalúa umbral (>=90% o no).
- Se informa el resultado con
onCompletion(Bool).