PulsingRecordButton.swift 2.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. import SwiftUI
  2. // MARK: - PulsingRecordButton
  3. /// A minimalist, line-drawn record button suitable for a business productivity app.
  4. /// Completely avoids heavy neon glows, replacing them with thin outlines, a clean scale animation,
  5. /// and an outline mic icon.
  6. struct PulsingRecordButton: View {
  7. /// Closure triggered on tap.
  8. let action: () -> Void
  9. var body: some View {
  10. TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { context in
  11. let time = context.date.timeIntervalSinceReferenceDate
  12. let breathingPhase = (sin(time * .pi) + 1) / 2
  13. let ripplePhase = time.truncatingRemainder(dividingBy: 2.5) / 2.5
  14. ZStack {
  15. // These transforms are derived directly from time instead of
  16. // using a repeating implicit animation. That keeps SwiftUI from
  17. // accidentally animating parent layout changes after a cover or
  18. // app lifecycle transition.
  19. Circle()
  20. .stroke(Color.primary.opacity(0.1), lineWidth: 1)
  21. .frame(width: 120, height: 120)
  22. .scaleEffect(0.98 + breathingPhase * 0.10)
  23. Circle()
  24. .stroke(Color.primary.opacity(0.08), lineWidth: 1)
  25. .frame(width: 100, height: 100)
  26. .scaleEffect(1.0 + ripplePhase * 0.25)
  27. .opacity(0.8 * (1.0 - ripplePhase))
  28. Circle()
  29. .fill(Color.primary.opacity(0.03))
  30. .frame(width: 90, height: 90)
  31. .overlay(
  32. Circle()
  33. .stroke(Color.primary.opacity(0.2), lineWidth: 1)
  34. )
  35. Image(systemName: "mic")
  36. .font(.system(size: 24, weight: .light))
  37. .foregroundColor(.primary)
  38. .scaleEffect(0.98 + breathingPhase * 0.04)
  39. }
  40. }
  41. .frame(width: 120, height: 120)
  42. .contentShape(Circle())
  43. .onTapGesture {
  44. HapticManager.trigger(.recordStart)
  45. action()
  46. }
  47. }
  48. }
  49. // MARK: - Preview
  50. #Preview {
  51. ZStack {
  52. Color.spaceBlack.ignoresSafeArea()
  53. PulsingRecordButton {
  54. print("Record tapped")
  55. }
  56. }
  57. }