Working better segmented control

This commit is contained in:
Hariharan Mudaliar 2025-09-10 21:02:28 +05:30
commit f8bdda0033

View file

@ -98,6 +98,12 @@ struct DraggableProgressBar: View {
var body: some View { var body: some View {
VStack { VStack {
GeometryReader { geo in GeometryReader { geo in
Group {
if Defaults[.progressBarStyle] == .segmented {
// Segmented progress bar - completely different layout
SegmentedProgressContent(value: value, geometry: geo)
} else {
// Traditional capsule-based progress bar
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
Capsule() Capsule()
.fill(.tertiary) .fill(.tertiary)
@ -114,11 +120,13 @@ struct DraggableProgressBar: View {
.frame(width: max(0, min(geo.size.width * value, geo.size.width))) .frame(width: max(0, min(geo.size.width * value, geo.size.width)))
.shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3) .shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3)
case .segmented: case .segmented:
SegmentedProgressContent(value: value, geometry: geo) EmptyView() // This case won't be reached due to the outer if condition
} }
} }
.opacity(value.isZero ? 0 : 1) .opacity(value.isZero ? 0 : 1)
} }
}
}
.gesture( .gesture(
DragGesture(minimumDistance: 0) DragGesture(minimumDistance: 0)
.onChanged { gesture in .onChanged { gesture in
@ -150,25 +158,100 @@ struct SegmentedProgressContent: View {
let value: CGFloat let value: CGFloat
let geometry: GeometryProxy let geometry: GeometryProxy
private let segmentCount = 20 private let segmentCount = 16
@State private var glowIndex: Int? = nil
@State private var lastValue: CGFloat = 0
var body: some View { var body: some View {
HStack(spacing: 1) { let spacing: CGFloat = 1.5
ForEach(0..<segmentCount, id: \.self) { index in let computed = (geometry.size.width - CGFloat(segmentCount - 1) * spacing) / CGFloat(segmentCount)
let segmentValue = CGFloat(index + 1) / CGFloat(segmentCount) let barWidth = max(3.0, min(6.0, computed))
let isActive = value >= segmentValue let activeCount = Int(round(value * CGFloat(segmentCount)))
RoundedRectangle(cornerRadius: 1) HStack(spacing: spacing) {
.fill(isActive ? ForEach(0..<segmentCount, id: \.self) { index in
(Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor] : .white) : RoundedRectangle(cornerRadius: 2)
.clear) .fill(segmentColor(isActive: index < activeCount))
.frame(width: max(1, (geometry.size.width - CGFloat(segmentCount - 1) * 1) / CGFloat(segmentCount))) .shadow(
.shadow(color: isActive && Defaults[.systemEventIndicatorShadow] ? color: Defaults[.systemEventIndicatorShadow]
(Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white) : ? glowShadowColor(for: index < activeCount, index: index)
.clear, radius: 4, x: 1) : .clear,
.opacity(value.isZero ? 0 : (isActive ? 1 : 0.3)) radius: Defaults[.systemEventIndicatorShadow]
? (glowIndex == index ? 12 : (index < activeCount ? 4 : 0))
: 0,
x: 0, y: 0
)
.frame(width: barWidth)
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: activeCount)
.scaleEffect(glowIndex == index ? 1.15 : 1.0)
.animation(.spring(response: 0.2, dampingFraction: 0.6), value: glowIndex == index)
} }
} }
.frame(width: geometry.size.width) .frame(maxWidth: .infinity, alignment: .center)
.opacity(value.isZero ? 0 : 1)
.onChange(of: value) { old, newVal in
handleGlow(oldValue: old, newValue: newVal)
}
}
private func segmentColor(isActive: Bool) -> Color {
if isActive {
if Defaults[.systemEventIndicatorUseAccent] {
return Defaults[.accentColor]
}
return .white
} else {
return .white.opacity(0.15)
}
}
private func glowShadowColor(for isActive: Bool, index: Int) -> Color {
if isActive {
if let glowIndex = glowIndex, index == glowIndex {
return Defaults[.systemEventIndicatorUseAccent]
? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.8)
: .white
} else {
return Defaults[.systemEventIndicatorUseAccent]
? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7)
: .white
}
}
return .clear
}
private func handleGlow(oldValue: CGFloat, newValue: CGFloat) {
defer { lastValue = newValue }
let oldIndex = Int(round(oldValue * CGFloat(segmentCount)))
let newIndex = Int(round(newValue * CGFloat(segmentCount)))
guard oldIndex != newIndex else { return }
if newIndex > oldIndex {
animateWave(from: oldIndex, to: newIndex, step: 1)
} else {
animateWave(from: oldIndex - 1, to: newIndex - 1, step: -1)
}
}
private func animateWave(from start: Int, to end: Int, step: Int) {
let clampedStart = max(0, min(segmentCount - 1, start))
let clampedEnd = max(-1, min(segmentCount - 1, end))
let range = stride(from: clampedStart, through: clampedEnd, by: step)
var delay: Double = 0
for i in range {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
withAnimation(.spring(response: 0.2, dampingFraction: 0.7)) {
glowIndex = i
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay + 0.2) {
if glowIndex == i {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
glowIndex = nil
}
}
}
delay += 0.015
}
} }
} }