How to achieve the following effect using SwiftUI?

Summary

A user on Stack Overflow asked how to implement an irregular selection effect on a circular layout in SwiftUI, inspired by the Pieoneer app. The user had the circular layout working but was stuck on the interaction. As a senior engineer, I would typically address this in a technical postmortem to analyze the common pitfalls of such UI implementations.

Root Cause

The inability to achieve the effect usually stems from a lack of understanding of SwiftUI’s hit-testing and view geometry. The specific issue is how to make non-rectangular shapes (like arcs of a circle) respond to touch events and how to map those touches to specific data items.

  • Misunderstanding Hit Testing: By default, SwiftUI’s .onTapGesture applies to the bounding box of the view, not just the visible path.
  • Missing Geometry Integration: Without explicit GeometryReader usage, calculating the distance from a touch point to the center of a circle becomes impossible.
  • State Management Overhead: Beginners often try to manage selection state in the parent view without properly passing binding values to sub-components.

Why This Happens in Real Systems

In real-world application development, drawing and interaction are often treated as separate domains. Developers are proficient with declarative layouts (HStack/VStack) but struggle when moving to custom drawing (Paths/Animations).

  • Separation of Concerns: Apple frameworks encourage separating view logic (SwiftUI) from drawing logic (CoreGraphics), but custom interactions require merging them.
  • Coordinate Systems: Touch events provide global coordinates, while circular layouts often require converting these to polar coordinates (angle and radius) to determine if a slice was tapped.
  • Animation Complexity: Irregular selection often implies morphing shapes. Beginners try to animate the path directly, which is computationally expensive, instead of animating a “selection indicator” overlay.

Real-World Impact

Failing to implement this correctly leads to poor user experience and performance issues.

  • Unresponsive UI: If hit testing is not custom-implemented, users might tap “dead zones” between slices and get no feedback.
  • Janky Animations: Incorrect use of @State vs. @GestureState causes animations to stutter or reset unexpectedly during rapid interactions.
  • High Cognitive Load: Users expect pie chart interactions to follow standard UX patterns (e.g., selection expanding outward). Deviating from this without proper feedback increases abandonment rates.

Example or Code (if necessary and relevant)

To implement a selectable slice in SwiftUI, you must combine a Path for drawing and a GeometryReader to handle coordinate conversion for touch detection.

import SwiftUI

struct PieSlice: View {
    var startAngle: Angle
    var endAngle: Angle
    var isSelected: Bool
    var color: Color
    var onTap: () -> Void

    var body: some View {
        GeometryReader { geometry in
            Path { path in
                let center = CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2)
                let radius = min(geometry.size.width, geometry.size.height) / 2
                path.move(to: center)
                path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
                path.closeSubpath()
            }
            .fill(color)
            .overlay(
                // Visual feedback for irregular selection
                isSelected ? 
                Circle()
                    .stroke(Color.white, lineWidth: 3)
                    .scaleEffect(1.1)
                    .transition(.scale) : nil
            )
            .contentShape(PathShape()) // Ensures hit testing follows the path
            .onTapGesture {
                onTap()
            }
        }
    }
}

// Helper to define the shape for hit testing
struct PathShape: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.addRect(rect)
        return path
    }
}

How Senior Engineers Fix It

Senior engineers approach this by decoupling the data model from the rendering logic and using geometry to map interactions.

  1. Implement Custom Hit Testing: Instead of relying on the default rectangle hit box, use Path.contains(point:) to determine if a touch falls within the visual slice.
  2. Use Polar Coordinates: Convert the touch point (x, y) into (angle, radius) relative to the center of the circle. This allows precise matching of the tap to the start/end angles of a slice.
  3. Decouple Visuals from Logic: Create a PieSliceShape struct conforming to Shape for the drawing. This allows reuse and better performance. The selection logic lives in the parent view, passed down via @Binding.
  4. Optimize Animation: Use matchedGeometryEffect if transitioning between states to ensure smooth morphing of the “irregular” shapes, rather than redrawing paths on every frame.

Why Juniors Miss It

Junior developers struggle with this because it requires a mental shift from layout-based thinking to canvas-based thinking.

  • Lack of Geometry Awareness: They often don’t realize that taps are registered in a Cartesian coordinate system (rectangles), while pie charts are Polar (angles/radius). They forget to convert the coordinates.
  • Over-reliance on System Gestures: They expect .onTapGesture to “just work” on custom shapes without realizing the view’s frame (the bounding box) is what actually receives the touch, not the visible pixel data.
  • Ignoring the View Graph: They try to calculate the interaction logic inside the drawing block of the Path, but drawing logic (Path) is separate from the interaction hierarchy (View).
  • State Scope Issues: They often place the @State for selection inside the slice view rather than the parent container, making it impossible to enforce the “single selection” logic typically required in a pie chart.