SlideToConfirmView.swift 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
  1. import SwiftUI
  2. struct SlideToConfirmView: View {
  3. let label: String
  4. let isEnabled: Bool
  5. let action: () -> Void
  6. @State private var dragOffset: CGFloat = 0
  7. private let thumbSize: CGFloat = 52
  8. private let trackHeight: CGFloat = 56
  9. private let completionThreshold: CGFloat = 0.85
  10. var body: some View {
  11. GeometryReader { geo in
  12. let maxDrag = geo.size.width - thumbSize - 8
  13. let progress = maxDrag > 0 ? dragOffset / maxDrag : 0
  14. ZStack(alignment: .leading) {
  15. Capsule()
  16. .fill(isEnabled ? Color.accentColor.opacity(0.25) : Color.secondary.opacity(0.2))
  17. Text(label)
  18. .font(.headline)
  19. .foregroundStyle(isEnabled ? .white.opacity(1 - progress) : .secondary)
  20. .frame(maxWidth: .infinity)
  21. RoundedRectangle(cornerRadius: thumbSize / 2)
  22. .fill(isEnabled ? Color.accentColor : Color.secondary.opacity(0.4))
  23. .frame(width: thumbSize, height: thumbSize)
  24. .overlay {
  25. Image(systemName: "chevron.right.2")
  26. .foregroundStyle(.white)
  27. .font(.system(size: 18, weight: .semibold))
  28. }
  29. .offset(x: 4 + dragOffset)
  30. .gesture(
  31. isEnabled ? DragGesture()
  32. .onChanged { value in
  33. dragOffset = min(max(0, value.translation.width), maxDrag)
  34. }
  35. .onEnded { _ in
  36. guard maxDrag > 0 else { return }
  37. if dragOffset >= maxDrag * completionThreshold {
  38. UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
  39. withAnimation(.spring()) { dragOffset = 0 }
  40. action()
  41. } else {
  42. withAnimation(.spring()) { dragOffset = 0 }
  43. }
  44. } : nil
  45. )
  46. }
  47. .frame(height: trackHeight)
  48. }
  49. .frame(height: trackHeight)
  50. }
  51. }