SwiftUI Segmented Control Component
The indicator slides between segments via matchedGeometryEffect rather than cutting. Same technique as the tab bar, deliberately — two controls that both express "one of these is selected" should move the same way.
$9.99 / month
Install the free SwiftUI segmented control
- Add SwiftKitUI.swift to your app once. It holds the tokens and motion curves.
- Copy SKSegmentedControl.swift below into your project. It needs nothing else.
Usage
Usage.swift
SKSegmentedControl(selection: $billing, options: Billing.allCases) { $0.title }Full source · 90 lines · MIT
SKSegmentedControl.swift
//
// SKSegmentedControl.swift
// Swift Kit UI
//
// Requires: SwiftKitUI.swift
//
// The indicator slides between segments via matchedGeometryEffect rather than
// cutting. Same technique as the tab bar, deliberately — two controls that
// both express "one of these is selected" should move the same way.
//
// Usage:
// SKSegmentedControl(selection: $billing, options: Billing.allCases) { $0.title }
//
import SwiftUI
public struct SKSegmentedControl<Option: Hashable>: View {
@Binding private var selection: Option
private let options: [Option]
private let title: (Option) -> String
@Namespace private var indicator
@Environment(\.accessibilityReduceMotion) private var reduceMotion
public init(
selection: Binding<Option>,
options: [Option],
title: @escaping (Option) -> String
) {
self._selection = selection
self.options = options
self.title = title
}
public var body: some View {
HStack(spacing: 0) {
ForEach(options, id: \.self) { option in
segment(option)
}
}
.padding(SK.Space.xs)
.background(SK.Palette.fill, in: Capsule(style: .continuous))
.accessibilityElement(children: .contain)
}
private func segment(_ option: Option) -> some View {
let isSelected = option == selection
return Button {
guard !isSelected else { return }
withAnimation(SKMotion.snap.resolved(reduceMotion: reduceMotion)) {
selection = option
}
SK.Haptic.play(.selection)
} label: {
Text(title(option))
.font(SK.Type_.callout)
.foregroundStyle(isSelected ? SK.Palette.label : SK.Palette.labelSecondary)
.frame(maxWidth: .infinity)
.padding(.vertical, SK.Space.sm)
.background {
if isSelected {
Capsule(style: .continuous)
.fill(SK.Palette.surfaceElevated)
.shadow(color: .black.opacity(0.10), radius: 4, y: 1)
.matchedGeometryEffect(id: "sk.segment.indicator", in: indicator)
}
}
.contentShape(.rect)
}
.buttonStyle(.plain)
.accessibilityAddTraits(isSelected ? [.isButton, .isSelected] : .isButton)
}
}
#Preview("SKSegmentedControl") {
@Previewable @State var billing = "Annual"
@Previewable @State var view = "Grid"
VStack(spacing: SK.Space.xl) {
SKSegmentedControl(selection: $billing, options: ["Monthly", "Annual"]) { $0 }
SKSegmentedControl(selection: $view, options: ["List", "Grid", "Map"]) { $0 }
Text(billing + " / " + view)
.font(SK.Type_.caption)
.foregroundStyle(SK.Palette.labelSecondary)
}
.padding(SK.Space.xl)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(SK.Palette.surfaceSunken)
}
Questions
- Is this SwiftUI segmented control free for commercial apps?
- Yes. Swift Kit UI is MIT licensed, so you can use it in personal and commercial apps with no payment or sign-up. Keep the licence notice with the code.
- What does it depend on?
- SKSegmentedControl.swift depends only on SwiftKitUI.swift, a single foundation file with the design tokens and motion curves. There are no third-party packages.
- Which iOS versions does it support?
- iOS 17 and later, plus macOS 14.
Related components
Building a whole app? The Swift Kit adds auth, payments and onboarding, ready to ship.