/// The observable model that drives ``RecordingView``.
///
/// The model implements the recording flow as a ``State`` machine: it exposes the visibility and icon of the view's controls for the current state, counts the
/// elapsed recording time, and processes the recorded input once it is sent.
@MainActor
@Observable
finalclassModel{
// MARK: Properties
/// The current state of the recording flow.
varstate:State=.notRecording
/// The number of seconds spent recording, excluding any time spent paused.
private(set)varsecondsElapsed:Int=0
/// The task that increments ``secondsElapsed`` every second while recording.
@ObservationIgnored
privatevartimerTask:Task<Void,Never>?
// MARK: Computed
/// The image resource for the main button: a pause icon while recording, a record icon otherwise.
/// Starts a recording when idle, pauses an ongoing recording, or resumes
/// a paused one. Does nothing while the input is being processed.
funcpressedMain(){
guardstate!=.processingelse{
return
}
switchstate{
case.notRecording:
state=.recording
case.recording:
state=.paused
case.paused:
state=.recording
case.processing:
break
}
}
/// Handles a press of the send button.
///
/// Moves a paused recording into processing. Does nothing in any other state.
funcpressedSend(){
guardstate==.pausedelse{
return
}
switchstate{
case.paused:
state=.processing
default:
break
}
}
/// Reacts to a change of ``state``, expected to be called from the view whenever it observes one.
///
/// 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
/// a new recording as opposed to one resuming from a pause.
funcupdatedState(
shouldRestartTimer:Bool
){
switchstate{
case.recording:
startTimer(shouldRestartTimer)
default:
stopTimer()
ifstate==.processing{
Task{
awaitprocessInput()
}
}
}
}
}
}
// MARK: - Helpers
privateextensionRecordingView.Model{
// MARK: Methods
/// Processes the recorded input, returning the model to the not-recording state when finished.
///
/// Currently a placeholder that simulates the work with a five-second delay.
funcprocessInput()async{
guardstate==.processingelse{
return
}
try?awaitTask.sleep(for:.seconds(2))
state=.notRecording
}
/// Starts the timer task, which increments ``secondsElapsed`` 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.
funcstartTimer(
_shouldRestartTimer:Bool
){
ifshouldRestartTimer{
secondsElapsed=0
}
timerTask?.cancel()
timerTask=Task{[weakself]in
while!Task.isCancelled{
try?awaitTask.sleep(for:.seconds(1))
guardletself,!Task.isCancelledelse{
return
}
self.secondsElapsed+=1
}
}
}
/// Stops the timer task, if any, keeping ``secondsElapsed`` at its current value.
funcstopTimer(){
timerTask?.cancel()
timerTask=nil
}
}
// MARK: - States
extensionRecordingView.Model{
/// The states of the recording flow.
enumState{
/// No recording is in progress.
casenotRecording
/// A recording is in progress and the timer is running.
caserecording
/// The recording is paused, and can be either resumed or sent.