From 2ae4306c3f7ab12ecc70ddac9842a8b49c5a1eaa Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 3 Jul 2026 19:08:42 +0200 Subject: [PATCH] Implemented the RecodingButtonStyle button style in the Recording package target. --- .../Recording/RecordingButtonStyle.swift | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 Packages/Features/Sources/Recording/RecordingButtonStyle.swift diff --git a/Packages/Features/Sources/Recording/RecordingButtonStyle.swift b/Packages/Features/Sources/Recording/RecordingButtonStyle.swift new file mode 100644 index 0000000..8eb4471 --- /dev/null +++ b/Packages/Features/Sources/Recording/RecordingButtonStyle.swift @@ -0,0 +1,143 @@ +import SwiftUI + +/// The button style for the controls of a ``RecordingView``. +/// +/// The style renders the button's label over a padded, circular red background, shrinks it slightly while pressed, and dims it while disabled. +struct RecordingButtonStyle: ButtonStyle { + + // MARK: Properties + + /// Whether the button allows user interaction. + @Environment(\.isEnabled) private var isEnabled + + // MARK: Methods + + /// Creates the styled body of the button. + /// + /// - Parameter configuration: The properties of the button being styled. + func makeBody( + configuration: Configuration + ) -> some View { + configuration.label + .frame( + width: Constant.Size.button, + height: Constant.Size.button + ) + .padding(Constant.Padding.button) + .background( + Circle() + .fill(.red) + ) + .opacity(opacity) + .scaleEffect(scaleEffect(configuration.isPressed)) + .animation( + .easeOut(duration: Constant.Animation.duration), + value: configuration.isPressed + ) + } + +} + +// MARK: - Helpers + +private extension RecordingButtonStyle { + + // MARK: Computed + + /// The opacity of the button: dimmed while disabled, fully opaque otherwise. + var opacity: Double { + isEnabled + ? Constant.Opacity.enabled + : Constant.Opacity.disabled + } + + // MARK: Methods + + /// Provides the scale of the button for a press state. + /// + /// - Parameter isPressed: Whether the button is currently pressed. + /// - Returns: A slightly shrunken scale while pressed, the resting scale otherwise. + func scaleEffect( + _ isPressed: Bool + ) -> CGFloat { + isPressed + ? Constant.Scale.pressed + : Constant.Scale.idle + } + +} + +// MARK: - Styles + +extension ButtonStyle where Self == RecordingButtonStyle { + + /// The button style for the controls of a ``RecordingView``. + static var recording: RecordingButtonStyle { + .init() + } + +} + +// MARK: - Constants + +/// The constant values used across the button style. +private enum Constant { + /// The animation constants. + enum Animation { + /// The duration of the press animation. + static let duration: TimeInterval = 0.15 + } + + /// The opacity constants. + enum Opacity { + /// The opacity of a disabled button. + static let disabled: Double = 0.4 + /// The opacity of an enabled button. + static let enabled: Double = 1 + } + + /// The padding constants. + enum Padding { + /// The padding between a button's label and its background. + static let button: CGFloat = 8 + } + + /// The scale constants. + enum Scale { + /// The scale of a button at rest. + static let idle: CGFloat = 1 + /// The scale of a button while pressed. + static let pressed: CGFloat = 0.85 + } + + /// The size constants. + enum Size { + /// The width and height of a button's label. + static let button: CGFloat = 24 + } +} + +// MARK: - Previews + +#Preview( + "Recording button style" +) { + @Previewable @State var isDisabled: Bool = false + + Button {} label: { + Image(.Icon.record) + .resizable() + } + .buttonStyle(.recording) + .disabled(isDisabled) + .safeAreaInset(edge: .bottom) { + Toggle( + "Button disabled", + isOn: $isDisabled + ) + .fontWeight(.semibold) + .dynamicTypeSize(.large) + .padding() + .tint(.red) + } +}