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+

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.