From f463c60b35aecc960fe9c1f7622d2ecf48d9de38 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 7 Jul 2026 00:38:07 +0200 Subject: [PATCH] Updated the RecordingButtonStyle button style in the Recording feature target to tint the round background color. --- Apps/Attendi/Catalogs/Localizable.xcstrings | 60 ++++++++++++++++ .../Sources/Services/ActivityReporting.swift | 16 ++++- Apps/Attendi/Sources/Views/ContentView.swift | 2 +- .../Styles/Buttons/RecordingButtonStyle.swift | 71 +++++-------------- .../Recording/Views/RecordingView.swift | 11 +-- 5 files changed, 99 insertions(+), 61 deletions(-) diff --git a/Apps/Attendi/Catalogs/Localizable.xcstrings b/Apps/Attendi/Catalogs/Localizable.xcstrings index d11fba0..ee7a36c 100644 --- a/Apps/Attendi/Catalogs/Localizable.xcstrings +++ b/Apps/Attendi/Catalogs/Localizable.xcstrings @@ -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", diff --git a/Apps/Attendi/Sources/Services/ActivityReporting.swift b/Apps/Attendi/Sources/Services/ActivityReporting.swift index 9f9f956..1bbcb8c 100644 --- a/Apps/Attendi/Sources/Services/ActivityReporting.swift +++ b/Apps/Attendi/Sources/Services/ActivityReporting.swift @@ -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( diff --git a/Apps/Attendi/Sources/Views/ContentView.swift b/Apps/Attendi/Sources/Views/ContentView.swift index d55af71..127e48c 100644 --- a/Apps/Attendi/Sources/Views/ContentView.swift +++ b/Apps/Attendi/Sources/Views/ContentView.swift @@ -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. diff --git a/Packages/Features/Sources/Recording/Styles/Buttons/RecordingButtonStyle.swift b/Packages/Features/Sources/Recording/Styles/Buttons/RecordingButtonStyle.swift index 07a2963..b117c19 100644 --- a/Packages/Features/Sources/Recording/Styles/Buttons/RecordingButtonStyle.swift +++ b/Packages/Features/Sources/Recording/Styles/Buttons/RecordingButtonStyle.swift @@ -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 diff --git a/Packages/Features/Sources/Recording/Views/RecordingView.swift b/Packages/Features/Sources/Recording/Views/RecordingView.swift index 1e96185..d6d58ee 100644 --- a/Packages/Features/Sources/Recording/Views/RecordingView.swift +++ b/Packages/Features/Sources/Recording/Views/RecordingView.swift @@ -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