Updated the RecordingButtonStyle button style in the Recording feature target to tint the round background color.
This commit is contained in:
@@ -1,6 +1,66 @@
|
|||||||
{
|
{
|
||||||
"sourceLanguage" : "en",
|
"sourceLanguage" : "en",
|
||||||
"strings" : {
|
"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" : {
|
"view.recording.navigation.title" : {
|
||||||
"comment" : "The title of the navigation bar in the recording view.",
|
"comment" : "The title of the navigation bar in the recording view.",
|
||||||
"extractionState" : "manual",
|
"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
|
/// 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
|
/// 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.
|
/// 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
|
@MainActor
|
||||||
final class ActivityReporting: Reporting {
|
final class ActivityReporting: Reporting {
|
||||||
|
|
||||||
@@ -30,7 +34,8 @@ final class ActivityReporting: Reporting {
|
|||||||
|
|
||||||
// MARK: Methods
|
// 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.
|
/// - Parameter anchor: The instant the elapsed recording time counts from.
|
||||||
func started(
|
func started(
|
||||||
@@ -41,6 +46,9 @@ final class ActivityReporting: Reporting {
|
|||||||
return
|
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(
|
activity = try? Activity.request(
|
||||||
attributes: RecordingActivityAttributes(),
|
attributes: RecordingActivityAttributes(),
|
||||||
content: .init(
|
content: .init(
|
||||||
@@ -55,7 +63,8 @@ final class ActivityReporting: Reporting {
|
|||||||
#endif
|
#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.
|
/// - Parameter elapsed: The number of seconds spent recording so far, excluding any time spent paused.
|
||||||
func paused(
|
func paused(
|
||||||
@@ -85,7 +94,8 @@ final class ActivityReporting: Reporting {
|
|||||||
#endif
|
#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.
|
/// - Parameter elapsed: The number of seconds spent recording, excluding any time spent paused.
|
||||||
func processing(
|
func processing(
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ private enum Constant {
|
|||||||
/// The key constants.
|
/// The key constants.
|
||||||
enum Key {
|
enum Key {
|
||||||
/// The user defaults key of the persisted locale identifier.
|
/// 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.
|
/// The spacing constants.
|
||||||
|
|||||||
@@ -2,20 +2,13 @@ import SwiftUI
|
|||||||
|
|
||||||
/// The button style for the controls of a ``RecordingView``.
|
/// 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
|
/// The style renders the button's label in the primary foreground color over a padded, circular Liquid Glass background — tinted by
|
||||||
/// background that reacts fluidly to presses, and dims the button while disabled. Both the label and its padding scale relative to the current
|
/// the color given at initialization — that reacts fluidly to presses. The whole button dims while disabled, and both the label and its
|
||||||
/// dynamic type size.
|
/// 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.
|
|
||||||
struct RecordingButtonStyle: ButtonStyle {
|
struct RecordingButtonStyle: ButtonStyle {
|
||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
/// The color scheme of the environment.
|
|
||||||
@Environment(\.colorScheme)
|
|
||||||
private var colorScheme
|
|
||||||
|
|
||||||
/// Whether the button allows user interaction.
|
/// Whether the button allows user interaction.
|
||||||
@Environment(\.isEnabled)
|
@Environment(\.isEnabled)
|
||||||
private var isEnabled
|
private var isEnabled
|
||||||
@@ -28,18 +21,18 @@ struct RecordingButtonStyle: ButtonStyle {
|
|||||||
@ScaledMetric
|
@ScaledMetric
|
||||||
private var size = Constant.Size.button
|
private var size = Constant.Size.button
|
||||||
|
|
||||||
/// Whether the color scheme of the button's label should be inverted from the environment's.
|
/// The color tinting the button's circular Liquid Glass background.
|
||||||
private let invertStyle: Bool
|
private let tintColor: Color
|
||||||
|
|
||||||
// MARK: Initializers
|
// 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.
|
/// - Parameter tintColor: The color tinting the button's circular background.
|
||||||
init(
|
fileprivate init(
|
||||||
invertStyle: Bool = false
|
tintColor: Color
|
||||||
) {
|
) {
|
||||||
self.invertStyle = invertStyle
|
self.tintColor = tintColor
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Methods
|
// MARK: Methods
|
||||||
@@ -58,14 +51,10 @@ struct RecordingButtonStyle: ButtonStyle {
|
|||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.foregroundStyle(.windowBackground.opacity(opacity))
|
.foregroundStyle(.primary.opacity(opacity))
|
||||||
.environment(
|
|
||||||
\.colorScheme,
|
|
||||||
colorSchemeLabel
|
|
||||||
)
|
|
||||||
.glassEffect(
|
.glassEffect(
|
||||||
.regular
|
.regular
|
||||||
.tint(.red)
|
.tint(tintColor)
|
||||||
.interactive(),
|
.interactive(),
|
||||||
in: .circle
|
in: .circle
|
||||||
)
|
)
|
||||||
@@ -80,17 +69,6 @@ private extension RecordingButtonStyle {
|
|||||||
|
|
||||||
// MARK: Computed
|
// 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.
|
/// The opacity of the button: dimmed while disabled, fully opaque otherwise.
|
||||||
var opacity: Double {
|
var opacity: Double {
|
||||||
isEnabled
|
isEnabled
|
||||||
@@ -104,19 +82,18 @@ private extension RecordingButtonStyle {
|
|||||||
|
|
||||||
extension ButtonStyle where Self == 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 {
|
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.
|
/// - Parameter tintColor: The color tinting the button's circular background.
|
||||||
/// - Returns: A recording button style with the given color scheme behavior.
|
|
||||||
static func recording(
|
static func recording(
|
||||||
invertStyle: Bool
|
tintColor: Color
|
||||||
) -> RecordingButtonStyle {
|
) -> RecordingButtonStyle {
|
||||||
.init(invertStyle: invertStyle)
|
.init(tintColor: tintColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -154,26 +131,16 @@ private enum Constant {
|
|||||||
@Previewable @State
|
@Previewable @State
|
||||||
var isDisabled: Bool = false
|
var isDisabled: Bool = false
|
||||||
|
|
||||||
@Previewable @State
|
|
||||||
var isStyleInverted: Bool = false
|
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
// Button action closure.
|
// Button action closure.
|
||||||
} label: {
|
} label: {
|
||||||
Image(.Icon.record)
|
Image(.Icon.record)
|
||||||
.resizable()
|
.resizable()
|
||||||
}
|
}
|
||||||
.buttonStyle(.recording(
|
.buttonStyle(.recording)
|
||||||
invertStyle: isStyleInverted
|
|
||||||
))
|
|
||||||
.disabled(isDisabled)
|
.disabled(isDisabled)
|
||||||
.safeAreaInset(edge: .bottom) {
|
.safeAreaInset(edge: .bottom) {
|
||||||
VStack {
|
VStack {
|
||||||
Toggle(
|
|
||||||
"Style inverted",
|
|
||||||
isOn: $isStyleInverted
|
|
||||||
)
|
|
||||||
|
|
||||||
Toggle(
|
Toggle(
|
||||||
"Button disabled",
|
"Button disabled",
|
||||||
isOn: $isDisabled
|
isOn: $isDisabled
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import SwiftUI
|
|||||||
/// A view that drives a recording session.
|
/// 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.
|
/// 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.
|
/// 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 {
|
public struct RecordingView: View {
|
||||||
@@ -92,7 +93,7 @@ public struct RecordingView: View {
|
|||||||
.resizable()
|
.resizable()
|
||||||
}
|
}
|
||||||
.buttonStyle(.recording(
|
.buttonStyle(.recording(
|
||||||
invertStyle: true
|
tintColor: .red
|
||||||
))
|
))
|
||||||
.accessibilityLabel(.viewRecordingButtonDiscardLabel)
|
.accessibilityLabel(.viewRecordingButtonDiscardLabel)
|
||||||
}
|
}
|
||||||
@@ -104,7 +105,7 @@ public struct RecordingView: View {
|
|||||||
.resizable()
|
.resizable()
|
||||||
}
|
}
|
||||||
.buttonStyle(.recording(
|
.buttonStyle(.recording(
|
||||||
invertStyle: true
|
tintColor: .yellow
|
||||||
))
|
))
|
||||||
.disabled(model.shouldDisableMain)
|
.disabled(model.shouldDisableMain)
|
||||||
.accessibilityLabel(model.labelMain)
|
.accessibilityLabel(model.labelMain)
|
||||||
@@ -121,7 +122,7 @@ public struct RecordingView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.recording(
|
.buttonStyle(.recording(
|
||||||
invertStyle: !model.isProcessing
|
tintColor: .green
|
||||||
))
|
))
|
||||||
.disabled(model.isProcessing)
|
.disabled(model.isProcessing)
|
||||||
.accessibilityLabel(model.labelSend)
|
.accessibilityLabel(model.labelSend)
|
||||||
@@ -177,7 +178,7 @@ public struct RecordingView: View {
|
|||||||
|
|
||||||
/// The constant values used across the view.
|
/// The constant values used across the view.
|
||||||
private enum Constant {
|
private enum Constant {
|
||||||
/// The spacing constants.
|
/// The size constants.
|
||||||
enum Size {
|
enum Size {
|
||||||
/// The font size of the timer label.
|
/// The font size of the timer label.
|
||||||
static let timer: CGFloat = 100
|
static let timer: CGFloat = 100
|
||||||
|
|||||||
Reference in New Issue
Block a user