What You Will Build
A grid of 12 color swatches that respond to taps with a spring scale animation. The selected color displays a white checkmark and stroke overlay, while a large preview card above the grid shows the current selection with a gradient fill and shadow that matches the chosen color.
Why This Pattern Matters
Color pickers are needed in note-taking apps, drawing tools, theme customization screens, and profile editors. This pattern teaches grid-based selection with visual feedback, matching shadows to dynamic content, and spring animations on selection changes.
Step 1: The Preview Card
The preview card at the top uses the selected color for its fill, text overlay, and shadow. The shadow color changes with the selection:
RoundedRectangle(cornerRadius: 20)
.fill(selectedColor.gradient)
.frame(height: 200)
.overlay {
Text("Selected Color")
.font(.title.bold())
.foregroundStyle(.white)
}
.padding(.horizontal)
.shadow(color: selectedColor.opacity(0.4), radius: 15, y: 8)
.animation(.spring, value: selectedColor)
Step 2: The Color Grid with Selection State
@State private var selectedColor: Color = .blue
let colors: [Color] = [
.red, .orange, .yellow, .green, .mint, .teal,
.cyan, .blue, .indigo, .purple, .pink, .brown,
]
LazyVGrid(columns: Array(repeating: GridItem(.flexible(),
spacing: 12), count: 6), spacing: 12) {
ForEach(colors, id: \.self) { color in
Circle()
.fill(color.gradient)
.frame(width: 44, height: 44)
.overlay {
if selectedColor == color {
Circle()
.stroke(.white, lineWidth: 3)
.shadow(radius: 3)
Image(systemName: "checkmark")
.font(.caption.bold())
.foregroundStyle(.white)
}
}
.scaleEffect(selectedColor == color ? 1.15 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.6),
value: selectedColor)
.onTapGesture {
selectedColor = color
}
}
}
.padding(.horizontal)
How the Selection Feedback Works
Three visual cues indicate the selected swatch:
- Scale: The selected circle scales to 1.15x, making it visibly larger than its neighbors.
- Checkmark: A white checkmark icon appears at the center.
- Stroke: A white border with a subtle shadow separates the selected circle from adjacent ones.
The spring animation with low dampingFraction (0.6) creates a slight bounce, giving the selection a playful, responsive feel.
Tips and Pitfalls
- Color equality: SwiftUI Color conforms to Equatable, so
selectedColor == colorworks directly. However, comparing colors created from custom RGB values may fail due to floating-point precision. Use an ID-based approach for custom colors. - .gradient modifier: Available in iOS 16+, this adds a subtle gradient to any solid color. For iOS 15, use
LinearGradientexplicitly. - Dynamic shadow: The
.shadow(color: selectedColor.opacity(0.4))on the preview card creates a colored glow effect that changes with each selection, adding perceived depth. - Haptic feedback: Add
UIImpactFeedbackGenerator(style: .light).impactOccurred()inside the onTapGesture for tactile confirmation. - Custom colors: To support arbitrary colors, model them as identifiable structs with id, name, and Color properties instead of using raw Color values.
iOS Version: iOS 16+ (uses .gradient modifier)