Reset the elapsed seconds after processing for the RecordingViewModel view model in the Recording package target.

This commit is contained in:
2026-07-03 20:30:37 +02:00
parent 851cedc54f
commit 4fd695c2cb
2 changed files with 36 additions and 24 deletions
@@ -17,11 +17,11 @@ extension RecordingView {
var state: State = .notRecording var state: State = .notRecording
/// The number of seconds spent recording, excluding any time spent paused. /// The number of seconds spent recording, excluding any time spent paused.
private(set) var secondsElapsed: Int = 0 private(set) var elapsedSeconds: Int = 0
/// The task that increments ``secondsElapsed`` every second while recording. /// The task that increments ``elapsedSeconds`` every second while recording.
@ObservationIgnored @ObservationIgnored
private var timerTask: Task<Void, Never>? private var taskTimer: Task<Void, Never>?
// MARK: Computed // MARK: Computed
@@ -65,7 +65,7 @@ extension RecordingView {
/// The elapsed recording time, formatted as `mm:ss` for the timer label. /// The elapsed recording time, formatted as `mm:ss` for the timer label.
var textTimer: String { var textTimer: String {
Duration Duration
.seconds(secondsElapsed) .seconds(elapsedSeconds)
.formatted(.time(pattern: .minuteSecond(padMinuteToLength: 2))) .formatted(.time(pattern: .minuteSecond(padMinuteToLength: 2)))
} }
@@ -112,7 +112,7 @@ extension RecordingView {
/// ///
/// Starts the timer when a recording begins or resumes, stops it in every other state, and kicks off ``processInput()`` when the input is sent. /// Starts the timer when a recording begins or resumes, stops it in every other state, and kicks off ``processInput()`` when the input is sent.
/// ///
/// - Parameter shouldRestartTimer: Whether ``secondsElapsed`` should be reset to zero before the timer starts, which is the case for /// - Parameter shouldRestartTimer: Whether ``elapsedSeconds`` should be reset to zero before the timer starts, which is the case for
/// a new recording as opposed to one resuming from a pause. /// a new recording as opposed to one resuming from a pause.
func updatedState( func updatedState(
shouldRestartTimer: Bool shouldRestartTimer: Bool
@@ -140,9 +140,9 @@ private extension RecordingView.Model {
// MARK: Methods // MARK: Methods
/// Processes the recorded input, returning the model to the not-recording state when finished. /// Processes the recorded input, resetting ``elapsedSeconds`` and returning the model to the not-recording state when finished.
/// ///
/// Currently a placeholder that simulates the work with a five-second delay. /// Currently a placeholder that simulates the work with a two-second delay.
func processInput() async { func processInput() async {
guard state == .processing else { guard state == .processing else {
return return
@@ -150,21 +150,22 @@ private extension RecordingView.Model {
try? await Task.sleep(for: .seconds(2)) try? await Task.sleep(for: .seconds(2))
elapsedSeconds = 0
state = .notRecording state = .notRecording
} }
/// Starts the timer task, which increments ``secondsElapsed`` once per second until it is cancelled. Any previously running timer task is cancelled first. /// Starts the timer task, which increments ``elapsedSeconds`` once per second until it is cancelled. Any previously running timer task is cancelled first.
/// ///
/// - Parameter shouldRestartTimer: Whether ``secondsElapsed`` should be reset to zero before the timer starts. /// - Parameter shouldRestartTimer: Whether ``elapsedSeconds`` should be reset to zero before the timer starts.
func startTimer( func startTimer(
_ shouldRestartTimer: Bool _ shouldRestartTimer: Bool
) { ) {
if shouldRestartTimer { if shouldRestartTimer {
secondsElapsed = 0 elapsedSeconds = 0
} }
timerTask?.cancel() taskTimer?.cancel()
timerTask = Task { [weak self] in taskTimer = Task { [weak self] in
while !Task.isCancelled { while !Task.isCancelled {
try? await Task.sleep(for: .seconds(1)) try? await Task.sleep(for: .seconds(1))
@@ -172,15 +173,15 @@ private extension RecordingView.Model {
return return
} }
self.secondsElapsed += 1 self.elapsedSeconds += 1
} }
} }
} }
/// Stops the timer task, if any, keeping ``secondsElapsed`` at its current value. /// Stops the timer task, if any, keeping ``elapsedSeconds`` at its current value.
func stopTimer() { func stopTimer() {
timerTask?.cancel() taskTimer?.cancel()
timerTask = nil taskTimer = nil
} }
} }
@@ -18,7 +18,7 @@ struct RecordingViewModelTests {
let model = Model() let model = Model()
#expect(model.state == .notRecording) #expect(model.state == .notRecording)
#expect(model.secondsElapsed == 0) #expect(model.elapsedSeconds == 0)
#expect(model.textTimer == "00:00") #expect(model.textTimer == "00:00")
} }
@@ -178,7 +178,7 @@ struct RecordingViewModelTests {
try await Task.sleep(for: .seconds(1.2)) try await Task.sleep(for: .seconds(1.2))
#expect(model.secondsElapsed >= 1) #expect(model.elapsedSeconds >= 1)
#expect(model.textTimer == "00:01") #expect(model.textTimer == "00:01")
model.state = .notRecording model.state = .notRecording
@@ -196,12 +196,12 @@ struct RecordingViewModelTests {
model.state = .paused model.state = .paused
model.updatedState(shouldRestartTimer: false) model.updatedState(shouldRestartTimer: false)
let secondsWhenPaused = model.secondsElapsed let secondsWhenPaused = model.elapsedSeconds
try await Task.sleep(for: .seconds(1.2)) try await Task.sleep(for: .seconds(1.2))
#expect(secondsWhenPaused >= 1) #expect(secondsWhenPaused >= 1)
#expect(model.secondsElapsed == secondsWhenPaused) #expect(model.elapsedSeconds == secondsWhenPaused)
} }
@Test func `keeps elapsed seconds when resumed`() async throws { @Test func `keeps elapsed seconds when resumed`() async throws {
@@ -215,12 +215,12 @@ struct RecordingViewModelTests {
model.state = .paused model.state = .paused
model.updatedState(shouldRestartTimer: false) model.updatedState(shouldRestartTimer: false)
let secondsWhenPaused = model.secondsElapsed let secondsWhenPaused = model.elapsedSeconds
model.state = .recording model.state = .recording
model.updatedState(shouldRestartTimer: false) model.updatedState(shouldRestartTimer: false)
#expect(model.secondsElapsed == secondsWhenPaused) #expect(model.elapsedSeconds == secondsWhenPaused)
model.state = .notRecording model.state = .notRecording
model.updatedState(shouldRestartTimer: false) model.updatedState(shouldRestartTimer: false)
@@ -240,7 +240,7 @@ struct RecordingViewModelTests {
model.state = .recording model.state = .recording
model.updatedState(shouldRestartTimer: true) model.updatedState(shouldRestartTimer: true)
#expect(model.secondsElapsed == 0) #expect(model.elapsedSeconds == 0)
model.state = .notRecording model.state = .notRecording
model.updatedState(shouldRestartTimer: false) model.updatedState(shouldRestartTimer: false)
@@ -254,15 +254,26 @@ struct RecordingViewModelTests {
@Suite("Processing") @Suite("Processing")
struct Processing { struct Processing {
@Test func `returns to not recording`() async throws { @Test func `returns to not recording with a reset timer`() async throws {
let model = Model() let model = Model()
model.state = .recording
model.updatedState(shouldRestartTimer: true)
try await Task.sleep(for: .seconds(1.2))
model.state = .paused
model.updatedState(shouldRestartTimer: false)
#expect(model.elapsedSeconds >= 1)
model.state = .processing model.state = .processing
model.updatedState(shouldRestartTimer: false) model.updatedState(shouldRestartTimer: false)
try await Task.sleep(for: .seconds(2.5)) try await Task.sleep(for: .seconds(2.5))
#expect(model.state == .notRecording) #expect(model.state == .notRecording)
#expect(model.elapsedSeconds == 0)
} }
} }