Last active
May 5, 2025 14:12
-
-
Save Koshimizu-Takehito/53e0f17b1a9e6172334333ba18c4bf08 to your computer and use it in GitHub Desktop.
A demo screen showcasing a custom color-based segmented control component.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
// MARK: - Demo Screen | |
/// A demo screen showcasing a custom color-based segmented control component. | |
/// Displays a list of segments and shows the selected segment's value in a large, bold title. | |
struct ColorSegmentedControlDemoScreen: View { | |
/// The static list of selectable segments. | |
private static let demoItems: [ColorItem] = [ | |
ColorItem(value: "Apple", color: .red), | |
ColorItem(value: "Orange", color: .orange.mix(with: .pink, by: 0.4)), | |
ColorItem(value: "Muscat", color: .green), | |
] | |
/// The currently selected segment binding. | |
@State private var selectedSegment = Self.demoItems[0] | |
var body: some View { | |
VStack { | |
ColorSegmentedControl(selection: $selectedSegment, items: Self.demoItems) | |
Text(String(describing: selectedSegment.value)) | |
.lineLimit(1) | |
.font(.largeTitle.bold()) | |
.foregroundStyle(selectedSegment.color) | |
} | |
.padding(.horizontal, 12) | |
.frame(maxWidth: .infinity, maxHeight: .infinity) | |
.background(selectedSegment.color.opacity(0.3)) | |
} | |
} | |
/// A selectable item with an associated display color. | |
struct ColorItem<Value: Hashable>: Identifiable, Hashable { | |
/// Unique identifier for diffing and animations. | |
var id = UUID() | |
/// The underlying value of this segment. | |
var value: Value | |
/// The highlight color for this segment when selected. | |
var color: Color | |
} | |
// MARK: - Color-Based Segmented Control | |
/// A generic, color-highlighted segmented control that animates | |
/// transitions between selections using SwiftUI’s matched geometry effect. | |
/// | |
/// Displays a capsule-shaped selection indicator behind the active segment. | |
/// `ColorItem`’s `id` and `color` are used to drive the animation and styling. | |
/// | |
/// - Parameters: | |
/// - selection: Two-way binding to the currently selected segment. | |
/// - items: The list of segments to render in this control. | |
struct ColorSegmentedControl<Value: Hashable>: View { | |
/// Namespace for matched-geometry animations between segments. | |
@Namespace private var namespace | |
/// Two-way binding to the currently selected segment. | |
@Binding var selection: ColorItem<Value> | |
/// The list of segments to render in this control. | |
var items: [ColorItem<Value>] | |
var body: some View { | |
ZStack { | |
SelectedSegment() | |
.matchedGeometryEffect(id: selection.id, in: namespace, isSource: false) | |
HStack { | |
ForEach(items) { item in | |
Segment(value: item.value, isSelected: selection == item) { | |
withAnimation(.spring(duration: 0.5, bounce: 0.3)) { | |
selection = item | |
} | |
} | |
.matchedGeometryEffect(id: item.id, in: namespace, isSource: true) | |
} | |
.padding(2) | |
} | |
} | |
.background(.tint.opacity(0.2)) | |
.background(.white) | |
.fixedSize(horizontal: false, vertical: true) | |
.clipShape(Capsule(style: .continuous)) | |
.tint(selection.color) | |
} | |
} | |
/// The capsule-shaped background for the selected segment. | |
private struct SelectedSegment: View { | |
var body: some View { | |
Capsule(style: .continuous) | |
.foregroundStyle(.tint) | |
.padding(2) | |
.shadow(radius: 2) | |
} | |
} | |
/// A single segment button with its text label and styling. | |
/// | |
/// - Parameters: | |
/// - value: The value displayed in this segment. | |
/// - isSelected: A Boolean indicating if this segment is the current selection. | |
/// - action: The closure to invoke when this segment is tapped. | |
private struct Segment<Value: Hashable>: View { | |
/// The model value for this segment. | |
var value: Value | |
/// Whether this segment is currently selected. | |
var isSelected: Bool | |
/// Action invoked when the button is tapped. | |
var action: () -> Void | |
var body: some View { | |
Button(action: action) { | |
Text(String(describing: value)) | |
.lineLimit(1) | |
.minimumScaleFactor(1) | |
.fontWeight(.bold) | |
.foregroundStyle(isSelected ? AnyShapeStyle(.white) : AnyShapeStyle(.tint)) | |
.padding() | |
.frame(maxWidth: .infinity) | |
} | |
} | |
} | |
// MARK: - Preview | |
#Preview { | |
ColorSegmentedControlDemoScreen() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment