Updated the RecordingButtonStyle button style in the Recording feature target to tint the round background color.

This commit is contained in:
2026-07-07 00:38:07 +02:00
parent dfd4829980
commit f463c60b35
5 changed files with 99 additions and 61 deletions
@@ -1,6 +1,66 @@
{
"sourceLanguage" : "en",
"strings" : {
"intent.discard-recording.description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Throws the paused recording away."
}
}
}
},
"intent.discard-recording.title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Discard Recording"
}
}
}
},
"intent.processing-recording.description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sends the paused recording for processing."
}
}
}
},
"intent.processing-recording.title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Process Recording"
}
}
}
},
"intent.toggle-recording.description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pauses or resumes the ongoing recording."
}
}
}
},
"intent.toggle-recording.title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pause or Resume Recording"
}
}
}
},
"view.recording.navigation.title" : {
"comment" : "The title of the navigation bar in the recording view.",
"extractionState" : "manual",
@@ -13,6 +13,10 @@ import Recording
/// resumption, and processing transition, and ends it dismissing it right away when the flow ends. The activity is an auxiliary
/// surface of the recording flow, never a required one: a start that fails, or that the user disallowed in the system settings, is
/// silently ignored, and the platforms without Live Activities reduce the whole service to a no-op.
///
/// Because a Live Activity outlives the process that started it, every running activity carries a stale date past which the system stops
/// presenting it as live, and ``reset()`` called once at launch ends any activity left over from an earlier run the app was killed
/// before it could end itself.
@MainActor
final class ActivityReporting: Reporting {
@@ -30,7 +34,8 @@ final class ActivityReporting: Reporting {
// MARK: Methods
/// Starts a Live Activity for a new recording, unless the user disallowed Live Activities or the system refuses to start one.
/// Starts a Live Activity for a new recording, unless the user disallowed Live Activities or the system refuses to start one. Any
/// activity still around from an earlier recording is ended first, so a desynced state can never leave the previous one behind.
///
/// - Parameter anchor: The instant the elapsed recording time counts from.
func started(
@@ -41,6 +46,9 @@ final class ActivityReporting: Reporting {
return
}
// End any activity still around before starting a new one, so a desynced state can never leak the previous activity.
await ended()
activity = try? Activity.request(
attributes: RecordingActivityAttributes(),
content: .init(
@@ -55,7 +63,8 @@ final class ActivityReporting: Reporting {
#endif
}
/// Updates the Live Activity with the pause of the ongoing recording, freezing its timer at the elapsed recording time.
/// Updates the Live Activity with the pause of the ongoing recording, freezing its timer at the elapsed recording time floored
/// to whole seconds, so the frozen value reads the same as the feature's own timer.
///
/// - Parameter elapsed: The number of seconds spent recording so far, excluding any time spent paused.
func paused(
@@ -85,7 +94,8 @@ final class ActivityReporting: Reporting {
#endif
}
/// Updates the Live Activity with the processing of the recorded input, freezing its timer at the elapsed recording time.
/// Updates the Live Activity with the processing of the recorded input, freezing its timer at the elapsed recording time floored
/// to whole seconds, so the frozen value reads the same as the feature's own timer.
///
/// - Parameter elapsed: The number of seconds spent recording, excluding any time spent paused.
func processing(
+1 -1
View File
@@ -225,7 +225,7 @@ private enum Constant {
/// The key constants.
enum Key {
/// The user defaults key of the persisted locale identifier.
static let locale = "com.rock-n-code.app.attendi.sample.user-defaults.ket.selected-locale"
static let locale = "com.rock-n-code.app.attendi.sample.user-defaults.key.selected-locale"
}
/// The spacing constants.
@@ -2,20 +2,13 @@ import SwiftUI
/// The button style for the controls of a ``RecordingView``.
///
/// The style renders the button's label, filled with the background color of the label's color scheme, over a padded, circular red Liquid Glass
/// background that reacts fluidly to presses, and dims the button while disabled. Both the label and its padding scale relative to the current
/// dynamic type size.
///
/// 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.
/// The style renders the button's label in the primary foreground color over a padded, circular Liquid Glass background tinted by
/// the color given at initialization that reacts fluidly to presses. The whole button dims while disabled, and both the label and its
/// padding scale relative to the current dynamic type size.
struct RecordingButtonStyle: ButtonStyle {
// MARK: Properties
/// The color scheme of the environment.
@Environment(\.colorScheme)
private var colorScheme
/// Whether the button allows user interaction.
@Environment(\.isEnabled)
private var isEnabled
@@ -28,18 +21,18 @@ struct RecordingButtonStyle: ButtonStyle {
@ScaledMetric
private var size = Constant.Size.button
/// Whether the color scheme of the button's label should be inverted from the environment's.
private let invertStyle: Bool
/// The color tinting the button's circular Liquid Glass background.
private let tintColor: Color
// MARK: Initializers
/// Creates a recording button style.
/// Creates a recording button style tinting the button's background with the given color.
///
/// - Parameter invertStyle: Whether the foreground style of the button's label should be inverted from the environment's.
init(
invertStyle: Bool = false
/// - Parameter tintColor: The color tinting the button's circular background.
fileprivate init(
tintColor: Color
) {
self.invertStyle = invertStyle
self.tintColor = tintColor
}
// MARK: Methods
@@ -58,14 +51,10 @@ struct RecordingButtonStyle: ButtonStyle {
.scaledToFit()
.padding(padding)
.scaledToFit()
.foregroundStyle(.windowBackground.opacity(opacity))
.environment(
\.colorScheme,
colorSchemeLabel
)
.foregroundStyle(.primary.opacity(opacity))
.glassEffect(
.regular
.tint(.red)
.tint(tintColor)
.interactive(),
in: .circle
)
@@ -80,17 +69,6 @@ private extension RecordingButtonStyle {
// MARK: Computed
/// 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
? .light
: .dark
}
/// The opacity of the button: dimmed while disabled, fully opaque otherwise.
var opacity: Double {
isEnabled
@@ -104,19 +82,18 @@ private extension RecordingButtonStyle {
extension ButtonStyle where Self == RecordingButtonStyle {
/// The button style for the controls of a ``RecordingView``, inverting the color scheme of the button's label.
/// The button style for the controls of a ``RecordingView``, tinted red.
static var recording: RecordingButtonStyle {
.init()
.init(tintColor: .red)
}
/// The button style for the controls of a ``RecordingView``.
/// The button style for the controls of a ``RecordingView``, tinted by the given color.
///
/// - 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.
/// - Parameter tintColor: The color tinting the button's circular background.
static func recording(
invertStyle: Bool
tintColor: Color
) -> RecordingButtonStyle {
.init(invertStyle: invertStyle)
.init(tintColor: tintColor)
}
}
@@ -154,26 +131,16 @@ private enum Constant {
@Previewable @State
var isDisabled: Bool = false
@Previewable @State
var isStyleInverted: Bool = false
Button {
// Button action closure.
} label: {
Image(.Icon.record)
.resizable()
}
.buttonStyle(.recording(
invertStyle: isStyleInverted
))
.buttonStyle(.recording)
.disabled(isDisabled)
.safeAreaInset(edge: .bottom) {
VStack {
Toggle(
"Style inverted",
isOn: $isStyleInverted
)
Toggle(
"Button disabled",
isOn: $isDisabled
@@ -7,7 +7,8 @@ import SwiftUI
/// A view that drives a recording session.
///
/// The view shows a main button that starts, pauses, and resumes a recording, with a label above it displaying the elapsed recording time.
/// While paused, a send button lets the user submit the recording for processing, showing a progress indicator until the processing finishes.
/// While paused, a discard button throws the recording away and a send button submits it for processing the send button showing a
/// progress indicator until the processing finishes.
///
/// All state and control behavior lives in the view's ``Model``; the view itself only renders it and forwards button presses.
public struct RecordingView: View {
@@ -92,7 +93,7 @@ public struct RecordingView: View {
.resizable()
}
.buttonStyle(.recording(
invertStyle: true
tintColor: .red
))
.accessibilityLabel(.viewRecordingButtonDiscardLabel)
}
@@ -104,7 +105,7 @@ public struct RecordingView: View {
.resizable()
}
.buttonStyle(.recording(
invertStyle: true
tintColor: .yellow
))
.disabled(model.shouldDisableMain)
.accessibilityLabel(model.labelMain)
@@ -121,7 +122,7 @@ public struct RecordingView: View {
}
}
.buttonStyle(.recording(
invertStyle: !model.isProcessing
tintColor: .green
))
.disabled(model.isProcessing)
.accessibilityLabel(model.labelSend)
@@ -177,7 +178,7 @@ public struct RecordingView: View {
/// The constant values used across the view.
private enum Constant {
/// The spacing constants.
/// The size constants.
enum Size {
/// The font size of the timer label.
static let timer: CGFloat = 100