2026-07-03 19:08:42 +02:00
import SwiftUI
/// The button style for the controls of a ``RecordingView``.
///
2026-07-03 20:00:14 +02:00
/// The style renders the button's label, filled with the background color of the label's color scheme, over a padded, circular red background, shrinks it
2026-07-03 19:42:32 +02:00
/// 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 20:00:14 +02:00
///
/// By default, the label's color scheme is inverted from the environment's, so its fill contrasts with the surroundings; this behavior can be turned off
/// through the ``init(invertStyle:)`` initializer.
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 20:53:55 +02:00
/// Whether the color scheme of the button's label should be inverted from the environment's.
private let invertStyle : Bool
2026-07-03 19:08:42 +02:00
2026-07-03 20:00:14 +02:00
// MARK: Initializers
/// Creates a recording button style.
///
/// - Parameter invertStyle: Whether the foreground style of the button's label should be inverted from the environment's.
init (
invertStyle : Bool = false
) {
self . invertStyle = invertStyle
}
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 ,
2026-07-03 20:00:14 +02:00
colorSchemeLabel
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 20:00:14 +02:00
/// The color scheme for the button's label: the opposite of the environment's when ``invertStyle`` is set, the environment's otherwise.
var colorSchemeLabel : ColorScheme {
guard invertStyle else {
return colorScheme
}
return colorScheme == . dark
2026-07-03 19:42:32 +02:00
? . 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 {
2026-07-03 20:00:14 +02:00
/// The button style for the controls of a ``RecordingView``, inverting the color scheme of the button's label.
2026-07-03 19:08:42 +02:00
static var recording : RecordingButtonStyle {
. init ()
}
2026-07-03 20:00:14 +02:00
/// The button style for the controls of a ``RecordingView``.
///
/// - Parameter invertStyle: Whether the color scheme of the button's label should be inverted from the environment's.
/// - Returns: A recording button style with the given color scheme behavior.
static func recording (
invertStyle : Bool
) -> RecordingButtonStyle {
. init ( invertStyle : invertStyle )
}
2026-07-03 19:08:42 +02:00
}
// 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
2026-07-03 20:00:14 +02:00
@ Previewable @ State var isStyleInverted : Bool = false
2026-07-03 19:08:42 +02:00
2026-07-03 20:00:14 +02:00
Button {
// Button action closure.
} label : {
2026-07-03 19:08:42 +02:00
Image (. Icon . record )
. resizable ()
}
2026-07-03 20:00:14 +02:00
. buttonStyle (. recording (
invertStyle : isStyleInverted
))
2026-07-03 19:08:42 +02:00
. disabled ( isDisabled )
. safeAreaInset ( edge : . bottom ) {
2026-07-03 20:00:14 +02:00
VStack {
Toggle (
"Style inverted" ,
isOn : $ isStyleInverted
)
Toggle (
"Button disabled" ,
isOn : $ isDisabled
)
}
2026-07-03 19:08:42 +02:00
. fontWeight (. semibold )
. dynamicTypeSize (. large )
. padding ()
. tint (. red )
}
}