// SPDX-License-Identifier: MIT // Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors import SwiftUI /// A segmented control with a Liquid Glass thumb that slides between segments. /// /// With Reduce Motion on, the thumb jumps to the new segment. @available(iOS 26, *) struct GlassSegmentedControl: View { let segments: [Segment] @Binding var selection: Segment var tint: Color? let title: (Segment) -> String @ScaledMetric(relativeTo: .subheadline) private var height = 44.0 @State private var width: CGFloat = 0 @Environment(\.accessibilityReduceMotion) private var reduceMotion @Environment(\.layoutDirection) private var layoutDirection var body: some View { HStack(spacing: 0) { ForEach(segments, id: \.self) { segment in button(for: segment) } } .background { thumb } .onGeometryChange(for: CGFloat.self) { $0.size.width } action: { width = $0 } // The thumb follows a finger dragged along the control, as the system's does. .simultaneousGesture(DragGesture(minimumDistance: 10).onChanged { select(at: $0.location.x) }) .padding(4) .background(.ultraThinMaterial, in: .capsule) .sensoryFeedback(.selection, trigger: selection) } /// One view that moves, drawn behind the labels so they stay readable on top of it. private var thumb: some View { GeometryReader { proxy in let width = proxy.size.width / CGFloat(max(segments.count, 1)) let index = segments.firstIndex(of: selection) ?? 0 Color.clear .frame(width: width) .glassEffect(.regular.tint(tint), in: .capsule) .offset(x: width * CGFloat(index)) } } private func select(at x: CGFloat) { guard width > 0, !segments.isEmpty else { return } // A gesture reports screen points, and a right-to-left layout counts from the other edge. let leading = layoutDirection == .rightToLeft ? width - x : x let index = min(max(Int(leading / width * CGFloat(segments.count)), 0), segments.count - 1) guard segments[index] != selection else { return } withAnimation(reduceMotion ? nil : .snappy(duration: 0.4)) { selection = segments[index] } } private func button(for segment: Segment) -> some View { let isSelected = segment == selection return Button { withAnimation(reduceMotion ? nil : .snappy(duration: 0.4)) { selection = segment } } label: { Text(title(segment)) .font(.subheadline.weight(isSelected ? .semibold : .medium)) .lineLimit(1) .frame(maxWidth: .infinity, minHeight: height) .contentShape(.capsule) } .buttonStyle(.plain) .accessibilityAddTraits(isSelected ? .isSelected : []) } } // MARK: - Demo @available(iOS 26, *) struct GlassSegmentedControlDemo: View { @State private var period = "Week" @Environment(\.accessibilityReduceMotion) private var reduceMotion private let steps = ["Day": 8_214, "Week": 61_730, "Month": 248_902, "Year": 2_904_117] var body: some View { ZStack { LinearGradient(colors: [.teal, .blue, .indigo], startPoint: .top, endPoint: .bottom) .ignoresSafeArea() VStack(spacing: 28) { VStack(spacing: 4) { Text("Steps walked") .font(.headline) .foregroundStyle(.white.opacity(0.8)) Text(steps[period, default: 0], format: .number) .font(.system(.largeTitle, design: .rounded, weight: .bold)) .contentTransition(.numericText()) .foregroundStyle(.white) .animation(reduceMotion ? nil : .snappy, value: period) } GlassSegmentedControl(segments: ["Day", "Week", "Month", "Year"], selection: $period) { $0 } .padding(.horizontal, 24) } } } } @available(iOS 26, *) #Preview { GlassSegmentedControlDemo() }