Implemented the (first version of the) Recording Live Activity in the Attendi widget target.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import AppIntents
|
||||
import Commanding
|
||||
import Recording
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
/// The buttons controlling the recording flow through the App Intents the app executes, matching the current state: a pause
|
||||
/// button while recording; discard, resume, and send buttons while paused; and only a discard button while the recorded input
|
||||
/// is being processed.
|
||||
struct RecordingControls: View {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The dynamic content of the Live Activity to render.
|
||||
let state: RecordingActivityAttributes.ContentState
|
||||
|
||||
// MARK: Body
|
||||
|
||||
var body: some View {
|
||||
HStack(
|
||||
spacing: Constant.Spacing.stack
|
||||
) {
|
||||
switch state.state {
|
||||
case .recording:
|
||||
Button(intent: ToggleRecordingIntent()) {
|
||||
Image.Symbol.pause
|
||||
.resizable()
|
||||
}
|
||||
.buttonStyle(.action(
|
||||
type: .default
|
||||
))
|
||||
|
||||
case .paused:
|
||||
Button(intent: DiscardRecordingIntent()) {
|
||||
Image.Symbol.discard
|
||||
.resizable()
|
||||
}
|
||||
.buttonStyle(.action(
|
||||
type: .destructive
|
||||
))
|
||||
|
||||
Button(intent: ToggleRecordingIntent()) {
|
||||
Image.Symbol.microphone
|
||||
.resizable()
|
||||
}
|
||||
.buttonStyle(.action(
|
||||
type: .default
|
||||
))
|
||||
|
||||
Button(intent: ProcessRecordingIntent()) {
|
||||
Image.Symbol.send
|
||||
.resizable()
|
||||
}
|
||||
.buttonStyle(.action(
|
||||
type: .confirmative
|
||||
))
|
||||
|
||||
case .processing:
|
||||
Button(intent: DiscardRecordingIntent()) {
|
||||
Image.Symbol.discard
|
||||
.resizable()
|
||||
}
|
||||
.buttonStyle(.action(
|
||||
type: .destructive
|
||||
))
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
/// The constant values used across the recording Live Activity.
|
||||
private enum Constant {
|
||||
/// The spacing constants.
|
||||
enum Spacing {
|
||||
/// The spacing between the elements of a stack.
|
||||
static let stack: CGFloat = 16
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview(
|
||||
"Recording Controls view",
|
||||
as: .content,
|
||||
using: RecordingActivityAttributes()
|
||||
) {
|
||||
RecordingLiveActivity()
|
||||
} contentStates: {
|
||||
RecordingActivityAttributes.ContentState(
|
||||
state: .recording,
|
||||
anchor: .now,
|
||||
elapsed: 0
|
||||
)
|
||||
RecordingActivityAttributes.ContentState(
|
||||
state: .paused,
|
||||
anchor: nil,
|
||||
elapsed: 42
|
||||
)
|
||||
RecordingActivityAttributes.ContentState(
|
||||
state: .processing,
|
||||
anchor: nil,
|
||||
elapsed: 42
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import Commanding
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
/// The Lock Screen banner of the recording Live Activity: a centered stack of the timer of the elapsed recording time, the label
|
||||
/// naming the current state, and the control buttons.
|
||||
struct RecordingLiveActivityView: View {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The dynamic content of the Live Activity to render.
|
||||
let state: RecordingActivityAttributes.ContentState
|
||||
|
||||
// MARK: Body
|
||||
|
||||
var body: some View {
|
||||
VStack(
|
||||
alignment: .center,
|
||||
spacing: Constant.Spacing.stack
|
||||
) {
|
||||
RecordingTimerText(state: state)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
.monospacedDigit()
|
||||
|
||||
RecordingStateLabel(state: state)
|
||||
|
||||
RecordingControls(state: state)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
/// The constant values used across the recording Live Activity.
|
||||
private enum Constant {
|
||||
/// The size constants.
|
||||
enum Size {
|
||||
/// The maximum width of the timer in the compact trailing presentation of the Dynamic Island.
|
||||
static let compactTimer: CGFloat = 44
|
||||
}
|
||||
|
||||
/// The spacing constants.
|
||||
enum Spacing {
|
||||
/// The spacing between the elements of a stack.
|
||||
static let stack: CGFloat = 8
|
||||
}
|
||||
|
||||
/// The system symbol constants.
|
||||
enum Symbol {
|
||||
/// The symbol of the discard button.
|
||||
static let discard = "trash"
|
||||
/// The symbol marking the recording flow.
|
||||
static let microphone = "mic.fill"
|
||||
/// The symbol of the main button while recording.
|
||||
static let pause = "pause.fill"
|
||||
/// The symbol of the main button while paused.
|
||||
static let record = "record.circle"
|
||||
}
|
||||
|
||||
/// The time constants.
|
||||
enum Time {
|
||||
/// The span of the running timer, comfortably beyond the maximum lifetime of a Live Activity.
|
||||
static let span: TimeInterval = 24 * 60 * 60
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview(
|
||||
"Recording Live Activity view",
|
||||
as: .content,
|
||||
using: RecordingActivityAttributes()
|
||||
) {
|
||||
RecordingLiveActivity()
|
||||
} contentStates: {
|
||||
RecordingActivityAttributes.ContentState(
|
||||
state: .recording,
|
||||
anchor: .now,
|
||||
elapsed: 0
|
||||
)
|
||||
RecordingActivityAttributes.ContentState(
|
||||
state: .paused,
|
||||
anchor: nil,
|
||||
elapsed: 42
|
||||
)
|
||||
RecordingActivityAttributes.ContentState(
|
||||
state: .processing,
|
||||
anchor: nil,
|
||||
elapsed: 42
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import Commanding
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
/// The label naming the current state of the recording flow, next to an icon matching that state: a microphone while recording,
|
||||
/// a pause symbol while paused, and a send symbol while the recorded input is being processed.
|
||||
struct RecordingStateLabel: View {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The dynamic content of the Live Activity to render.
|
||||
let state: RecordingActivityAttributes.ContentState
|
||||
|
||||
// MARK: Body
|
||||
|
||||
var body: some View {
|
||||
Label {
|
||||
switch state.state {
|
||||
case .recording:
|
||||
Text(.viewRecordingStateLabelStateRecording)
|
||||
case .paused:
|
||||
Text(.viewRecordingStateLabelStatePaused)
|
||||
case .processing:
|
||||
Text(.viewRecordingStateLabelStateProcessing)
|
||||
}
|
||||
} icon: {
|
||||
switch state.state {
|
||||
case .recording:
|
||||
Image.Symbol.microphone
|
||||
.foregroundStyle(.yellow)
|
||||
case .paused:
|
||||
Image.Symbol.pause
|
||||
.foregroundStyle(.yellow)
|
||||
case .processing:
|
||||
Image.Symbol.send
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
}
|
||||
.font(.body)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview(
|
||||
"Recording State label",
|
||||
as: .dynamicIsland(.expanded),
|
||||
using: RecordingActivityAttributes()
|
||||
) {
|
||||
RecordingLiveActivity()
|
||||
} contentStates: {
|
||||
RecordingActivityAttributes.ContentState(
|
||||
state: .recording,
|
||||
anchor: .now,
|
||||
elapsed: 0
|
||||
)
|
||||
RecordingActivityAttributes.ContentState(
|
||||
state: .paused,
|
||||
anchor: nil,
|
||||
elapsed: 325
|
||||
)
|
||||
RecordingActivityAttributes.ContentState(
|
||||
state: .processing,
|
||||
anchor: nil,
|
||||
elapsed: 325
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import Commanding
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
/// The timer of the elapsed recording time, formatted as `mm:ss`: counted up by the system from the content state's anchor when
|
||||
/// one is given, and frozen at the reported elapsed time otherwise.
|
||||
struct RecordingTimerText: View {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The dynamic content of the Live Activity to render.
|
||||
let state: RecordingActivityAttributes.ContentState
|
||||
|
||||
// MARK: Body
|
||||
|
||||
var body: some View {
|
||||
if let anchor = state.anchor {
|
||||
Text(
|
||||
.durationOffset(to: anchor),
|
||||
format: timerFormat
|
||||
)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
else {
|
||||
Text(
|
||||
Duration
|
||||
.seconds(state.elapsed)
|
||||
.formatted(timerFormat)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension RecordingTimerText {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The format of the timer: minutes and seconds, as `mm:ss`.
|
||||
var timerFormat: Duration.TimeFormatStyle {
|
||||
.time(pattern: .minuteSecond(padMinuteToLength: 2))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview(
|
||||
"Recording Timer text",
|
||||
as: .dynamicIsland(.compact),
|
||||
using: RecordingActivityAttributes()
|
||||
) {
|
||||
RecordingLiveActivity()
|
||||
} contentStates: {
|
||||
RecordingActivityAttributes.ContentState(
|
||||
state: .recording,
|
||||
anchor: .now,
|
||||
elapsed: 0
|
||||
)
|
||||
RecordingActivityAttributes.ContentState(
|
||||
state: .paused,
|
||||
anchor: nil,
|
||||
elapsed: 325
|
||||
)
|
||||
RecordingActivityAttributes.ContentState(
|
||||
state: .processing,
|
||||
anchor: nil,
|
||||
elapsed: 325
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user