Skip to content

Instantly share code, notes, and snippets.

@durul
Last active October 13, 2025 13:53
Show Gist options
  • Select an option

  • Save durul/5af30ac5a2379c0e569709558b1911a6 to your computer and use it in GitHub Desktop.

Select an option

Save durul/5af30ac5a2379c0e569709558b1911a6 to your computer and use it in GitHub Desktop.
SwiftUI: Segmented “Pill” Control (Location / Settings)

✅ SwiftUI: Segmented “Pill” Control (Location / Settings)

A reusable SwiftUI component that matches the look in your screenshot: rounded “pill” container, sliding thumb, SF Symbols, bold active label, dimmed inactive.

import SwiftUI

// MARK: - Model
enum SegTab: String, CaseIterable, Identifiable {
    case location = "Location"
    case settings = "Settings"
    var id: String { rawValue }
    
    var icon: String {
        switch self {
        case .location: return "mappin.and.ellipse"   // or "location.circle"
        case .settings: return "gearshape"
        }
    }
}

// MARK: - Control
struct PillSegmentedControl<T: Hashable & Identifiable & CaseIterable>: View {
    struct Item: Identifiable, Hashable {
        var id: T
        var title: String
        var systemImage: String
    }
    
    @Binding var selection: T
    var items: [Item]
    var height: CGFloat = 56
    var cornerRadius: CGFloat = 28
    var padding: CGFloat = 6
    
    @Namespace private var thumbNS
    
    var body: some View {
        GeometryReader { geo in
            let w = geo.size.width
            let h = max(geo.size.height, height)
            let segmentW = (w - padding * 2) / CGFloat(items.count)
            
            ZStack(alignment: .leading) {
                // Background
                RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
                    .fill(.black.opacity(0.65))
                    .overlay(
                        RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
                            .stroke(.white.opacity(0.08), lineWidth: 1)
                    )
                    .shadow(color: .black.opacity(0.35), radius: 16, y: 6)
                
                // Thumb
                if let idx = items.firstIndex(where: { $0.id == selection }) {
                    RoundedRectangle(cornerRadius: cornerRadius - 4, style: .continuous)
                        .fill(
                            LinearGradient(colors: [
                                Color.white.opacity(0.08),
                                Color.white.opacity(0.02)
                            ], startPoint: .topLeading, endPoint: .bottomTrailing)
                            .blendMode(.plusLighter)
                        )
                        .background(
                            RoundedRectangle(cornerRadius: cornerRadius - 4, style: .continuous)
                                .fill(Color.white.opacity(0.06))
                        )
                        .overlay(
                            RoundedRectangle(cornerRadius: cornerRadius - 4, style: .continuous)
                                .stroke(.white.opacity(0.12), lineWidth: 1)
                        )
                        .frame(width: segmentW, height: h - padding * 2)
                        .padding(padding)
                        .offset(x: CGFloat(idx) * segmentW)
                        .matchedGeometryEffect(id: "thumb", in: thumbNS)
                        .animation(.snappy(duration: 0.28), value: selection)
                }
                
                // Segments
                HStack(spacing: 0) {
                    ForEach(items) { item in
                        Button {
                            UIImpactFeedbackGenerator(style: .light).impactOccurred()
                            withAnimation(.snappy(duration: 0.28)) {
                                selection = item.id
                            }
                        } label: {
                            HStack(spacing: 10) {
                                Image(systemName: item.systemImage)
                                    .font(.system(size: 20, weight: .semibold))
                                Text(item.title)
                                    .font(.system(.headline, design: .rounded))
                                    .fontWeight(.semibold)
                            }
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                            .contentShape(Rectangle())
                        }
                        .buttonStyle(.plain)
                        .foregroundStyle(selection == item.id ? Color.white : Color.white.opacity(0.5))
                        .accessibilityLabel(Text(item.title))
                        .accessibilityAddTraits(selection == item.id ? .isSelected : [])
                    }
                }
                .padding(padding)
            }
            .frame(height: h)
        }
        .frame(height: height)
    }
}

// MARK: - Convenience wrapper for your two tabs
struct LocationSettingsSegmented: View {
    @Binding var selection: SegTab
    
    var body: some View {
        PillSegmentedControl(
            selection: $selection,
            items: SegTab.allCases.map { .init(id: $0, title: $0.rawValue, systemImage: $0.icon) },
            height: 56,
            cornerRadius: 28,
            padding: 6
        )
        .padding(.horizontal, 16)
        .environment(\.colorScheme, .dark) // matches your dark look
    }
}

// MARK: - Demo
struct ContentView: View {
    @State private var tab: SegTab = .location
    var body: some View {
        VStack(spacing: 24) {
            Text("PickerStyle - Segmented")
                .font(.caption)
                .foregroundStyle(.white.opacity(0.7))
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding(.horizontal, 24)
            
            LocationSettingsSegmented(selection: $tab)
            
            // Example of reacting to selection
            Group {
                if tab == .location {
                    Text("Location view")
                } else {
                    Text("Settings view")
                }
            }
            .foregroundStyle(.white.opacity(0.9))
            .padding(.top, 8)
        }
        .padding(.vertical, 32)
        .background(Color(red: 0.12, green: 0.12, blue: 0.12))
    }
}

#Preview {
    ContentView()
}

Notes

  • Uses matchedGeometryEffect for the smooth sliding thumb.
  • Dark, slightly glossy style to mirror the screenshot.
  • Works on iOS 17+/macOS 14+/visionOS; swap symbols if you prefer (location.circle vs mappin.and.ellipse).
  • Tweak height, cornerRadius, and padding to fit your layout.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment