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) } }