2026-07-03 19:08:42 +02:00
import SwiftUI
/// The button style for the controls of a ``RecordingView``.
///
2026-07-03 19:42:32 +02:00
/// The style renders the button's label, filled with the background color of the inverted color scheme, over a padded, circular red background, shrinks it
/// slightly while pressed, and dims it while disabled. Both the label and its padding scale relative to the current dynamic type size.
2026-07-03 19:08:42 +02:00
struct RecordingButtonStyle : ButtonStyle {
// MARK: Properties
2026-07-03 19:42:32 +02:00
/// The color scheme of the environment.
@ Environment ( \ . colorScheme ) private var colorScheme
2026-07-03 19:08:42 +02:00
/// Whether the button allows user interaction.
@ Environment ( \ . isEnabled ) private var isEnabled
2026-07-03 19:42:32 +02:00
/// The padding between the button's label and its background, scaled relative to the current dynamic type size.
@ ScaledMetric private var padding = Constant . Padding . button
/// The width and height of the button's label, scaled relative to the current dynamic type size.
@ ScaledMetric private var size = Constant . Size . button
2026-07-03 19:08:42 +02:00
// 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 (
2026-07-03 19:42:32 +02:00
width : size ,
height : size
)
. padding ( padding )
. scaledToFit ()
. foregroundStyle (. background )
. environment (
\ . colorScheme ,
colorSchemeInverted
2026-07-03 19:08:42 +02:00
)
. 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
2026-07-03 19:42:32 +02:00
/// The color scheme opposite to the environment's, under which the label's background fill resolves to the inverted background color.
var colorSchemeInverted : ColorScheme {
colorScheme == . dark
? . light
: . dark
}
2026-07-03 19:08:42 +02:00
/// 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 {
2026-07-03 19:42:32 +02:00
/// The base padding between a button's label and its background, before dynamic type scaling.
2026-07-03 19:08:42 +02:00
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 {
2026-07-03 19:42:32 +02:00
/// The base width and height of a button's label, before dynamic type scaling.
2026-07-03 19:09:09 +02:00
static let button : CGFloat = 16
2026-07-03 19:08:42 +02:00
}
}
// 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 )
}
}