Skip to content

Instantly share code, notes, and snippets.

@seifscape
Created July 15, 2026 18:18
Show Gist options
  • Select an option

  • Save seifscape/5b2e7ab6e704510dce8ffc6defbacc97 to your computer and use it in GitHub Desktop.

Select an option

Save seifscape/5b2e7ab6e704510dce8ffc6defbacc97 to your computer and use it in GitHub Desktop.
Reusable animated segmented control in SwiftUI (config-driven, generic over segment type)
import SwiftUI
struct SegmentedView<Segment: Hashable & CustomStringConvertible>: View {
struct Config {
var tint: Color = .green
var inactiveColor: Color = Color(.systemGray)
var font: Font = .footnote.weight(.medium)
var underlineHeight: CGFloat = 4
var spacing: CGFloat = 6
var animation: Animation = .spring(response: 0.3, dampingFraction: 0.8)
static var `default`: Config { Config() }
}
let segments: [Segment]
@Binding var selected: Segment
var config: Config = .default
@Namespace private var underlineNamespace
var body: some View {
HStack(spacing: 0) {
ForEach(segments, id: \.self) { segment in
let isSelected = segment == selected
Button {
withAnimation(config.animation) {
selected = segment
}
} label: {
VStack(spacing: config.spacing) {
Text(segment.description)
.font(config.font)
.foregroundColor(isSelected ? config.tint : config.inactiveColor)
ZStack {
Capsule()
.fill(Color.clear)
.frame(height: config.underlineHeight)
if isSelected {
Capsule()
.fill(config.tint)
.frame(height: config.underlineHeight)
.matchedGeometryEffect(id: "underline", in: underlineNamespace)
}
}
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.plain)
.accessibilityAddTraits(isSelected ? .isSelected : [])
}
}
.accessibilityElement(children: .contain)
}
}
// MARK: - Usage
private enum Status: String, CaseIterable, CustomStringConvertible {
case open = "OPEN", completed = "COMPLETED", cancelled = "CANCELLED", all = "ALL"
var description: String { rawValue }
}
private struct ContentView: View {
@State private var selected: Status = .open
var body: some View {
SegmentedView(segments: Status.allCases, selected: $selected)
// Custom styling:
// SegmentedView(segments: Status.allCases, selected: $selected,
// config: .init(tint: .blue, inactiveColor: .secondary))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment