The tearDistortion shader receives, per pixel, the current position plus parameters coming from SwiftUI:
tearStart: where the tear starts (on the real edge of the view).tearEnd: current finger point (projected onto the fixed tear line).tearProgress: overall gesture progress (0...1).maxGap: maximum opening between the two halves.edgeRoughness: irregularity of the torn edge.viewSize: view size for safe clipping.
Internal shader flow:
- If
tearProgressis near 0, it leaves the pixel unchanged (layer.sample(position)). - It builds the tear line using
tearDir = tearEnd - tearStart. - It derives two axes:
tearNorm: axis along the cut.tearPerp: axis perpendicular to the cut.
- For each pixel, it computes:
alongTear: how far the pixel is along the tear line.acrossTear: perpendicular distance to the tear line.
- It defines a wedge-shaped opening:
- widest at
tearStart. - zero at
tearEnd.
- It applies sinusoidal noise to simulate torn fibers (
edgeRoughness). - If the pixel falls inside the gap (
absDist < halfGap), it becomes transparent. - If it is outside the gap, sampling is shifted to separate both halves visually.
- If sampling goes out of
viewSize, it returns transparent to avoid artifacts.
Result: a clean tear with separated halves and rough edges, without curl/warp deformation.
The integration is split across:
TearState.swift: state + geometric computations.TearModifier.swift: gesture orchestration + shader uniforms.Extensions/View+TearKitExtensions.swift: publictearable(...)API.
Key variables:
edgePoint: real tear anchor point on the edge.fingerPoint: current tear end point (projected finger).fixedDirection: unit direction locked at gesture start.directionLocked: whether direction is already locked.gapWidth: opening sent asmaxGap.tearProgress: progress sent to the shader.lockProjection: lock-time baseline projection to avoid jumps.maxTrajectoryDistance: maximum tear-line distance available inside the view.hasFullyTorn: prevents re-triggering once fully torn.
In body(content:):
geometryReaderstoresviewSize.layerEffectcreates aShader(function:arguments:)fortearDistortionand passes state values.isEnabledactivates the shader only when:
- gesture is active,
- direction is locked,
- and there is a real opening (
gapWidth > 0.1).
- The modifier does not apply drop/opacity effects; those reactions are delegated to the host view via callbacks.
- On first
onChanged, ifstartLocationis near an edge:
- it activates state,
- initializes
edgePointandfingerPointwith that start location.
- While direction is not locked (
directionLocked == false):
- it waits until drag exceeds ~20pt,
- then calls
lockDirection(from:to:cutDirection:).
lockDirection:
- computes unit direction,
- raycasts backward to get the real edge intersection (
edgePoint), - stores
lockProjectionas a baseline.
- During this initial phase:
tearProgress = 0,gapWidth = 0,- so no visible effect appears.
- Once direction is locked:
- finger is projected with
projectedEnd(fingerLocation:), - projection is clamped to the valid in-view segment,
- distance, progress, and gap are updated.
- On
onEnded:
- if
tearProgress >= breakThreshold(default0.9): marks full break and notifies via callback. - otherwise: animated recovery.
Decision is made when the gesture ends:
- Compare
tearProgressagainstbreakThreshold.tearProgressis computed as:- traveled distance along the fixed line
- divided by the maximum distance of that same line inside the view.
- If it exceeds threshold:
- force
tearProgress = 1and max gap, - trigger
onCompletion(true).- The final action/animation (fall, fade, navigation, etc.) is defined by the view using the modifier.
- If it does not exceed threshold:
- first animate closing the cut frame-by-frame (explicit interpolation of finger/progress/gap to 0),
- then perform internal
reset(), - trigger
onCompletion(false).
The tearable(...) extension now exposes:
edgeRoughness: torn-edge irregularity strength.maxGapWidth: maximum opening near tear start.breakThreshold: full-break threshold (0...1).cutDirection: tear direction mode (.vertical,.horizontal,.anyDirection).isBroken: optionalBinding<Bool>to observe/control the broken state.onProgressChanged: continuous progress callback (0...1).onCompletion: final result callback.
Quick cutDirection examples:
.vertical: constrained to vertical cuts only..horizontal: constrained to horizontal cuts only..anyDirection: follows the initial drag direction.
onCompletion semantics:
true: view fully broke and falls.false: threshold not reached and view recovers.
isBroken semantics:
- set
falseto trigger recovery/reset. - set
trueto force broken state externally.
- User drags from an edge.
- SwiftUI updates
TearState. TearModifiersends uniforms to the shader.tearDistortioncomputes gap + per-pixel separation.- On release, threshold is evaluated (>=90% or not).
- Final result is reported via
onCompletion(Bool).