What You Will Build
A grid of rounded rectangles that magnify near your finger as you drag, creating a fisheye lensing effect. Apple uses this on the Apple Watch home screen and macOS Dock.
Key Concepts
- @GestureState tracks drag position and auto-resets when the finger lifts.
- GeometryReader + coordinateSpace gets each cell position relative to the touch.
- Euclidean distance scaling — cells within 150pt scale up, farther cells shrink.
The Distance Calculation
func touchItemScale(rect: CGRect, size: CGSize) -> CGFloat {
let a = touchLocation.x - rect.midX
let b = touchLocation.y - rect.midY
let distance = sqrt((a * a) + (b * b))
let scale = (distance - 150) / 150
let result = touchLocation == .zero ? 1 : (1 - scale)
return max(result, 0.001)
}
The Grid with Gradient Mask
LinearGradient(
colors: [.cyan, .yellow, .mint, .pink, .purple],
startPoint: .topLeading, endPoint: .bottomTrailing
)
.mask {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 0),
count: 10), spacing: 0) {
ForEach(0..<200, id: \.self) { _ in
GeometryReader { geo in
let rect = geo.frame(in: .named("grid"))
RoundedRectangle(cornerRadius: 4)
.fill(.white)
.scaleEffect(touchItemScale(rect: rect, size: geo.size))
}
.aspectRatio(1, contentMode: .fit)
}
}
}
.coordinateSpace(name: "grid")
.gesture(
DragGesture(minimumDistance: 0)
.updating($touchLocation) { value, state, _ in
state = value.location
}
)
Tips
- Use @GestureState (auto-resets) not @State (needs manual cleanup).
- coordinateSpace is essential for correct position calculations.
iOS Version: iOS 15+