Created the Commanding package target in the Features package for controlling the recording flow from Live Activities.
This commit is contained in:
@@ -11,23 +11,40 @@ let package = Package(
|
||||
.visionOS(.v26),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "Commanding",
|
||||
targets: [
|
||||
"Commanding",
|
||||
]
|
||||
),
|
||||
.library(
|
||||
name: "Features",
|
||||
targets: [
|
||||
"Commanding",
|
||||
"Notifying",
|
||||
"Recording",
|
||||
]
|
||||
),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "Commanding",
|
||||
path: "Sources/Commanding"
|
||||
),
|
||||
.target(
|
||||
name: "Notifying",
|
||||
path: "Sources/Notifying"
|
||||
),
|
||||
.target(
|
||||
name: "Recording",
|
||||
dependencies: ["Commanding"],
|
||||
path: "Sources/Recording"
|
||||
),
|
||||
.testTarget(
|
||||
name: "CommandingTests",
|
||||
dependencies: ["Commanding"],
|
||||
path: "Tests/Commanding"
|
||||
),
|
||||
.testTarget(
|
||||
name: "NotifyingTests",
|
||||
dependencies: ["Notifying"],
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#if os(iOS)
|
||||
import ActivityKit
|
||||
import Foundation
|
||||
|
||||
/// The attributes of the Live Activity that surfaces the recording flow on the Lock Screen and in the Dynamic Island.
|
||||
///
|
||||
/// The attributes carry no static content: everything the activity shows lives in its ``ContentState``, which the app's reporting
|
||||
/// service updates on every transition of the recording flow. The type is shared between the app — which starts, updates, and
|
||||
/// ends the activity — and the widget extension — which renders it.
|
||||
public struct RecordingActivityAttributes: ActivityAttributes {
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates the attributes of the Live Activity of a recording flow.
|
||||
public init() {}
|
||||
|
||||
// MARK: Content state
|
||||
|
||||
/// The dynamic content of the Live Activity, updated on every transition of the recording flow.
|
||||
public struct ContentState: Codable, Hashable, Sendable {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The current state of the recording flow.
|
||||
public var state: RecordingActivityState
|
||||
|
||||
/// The instant the elapsed recording time counts from — moved back by the time already spent recording, so a timer
|
||||
/// counting up from it shows the elapsed recording time — or `nil` while the recording is not running.
|
||||
public var anchor: Date?
|
||||
|
||||
/// The number of seconds spent recording up to the last transition, excluding any time spent paused, shown as a frozen
|
||||
/// elapsed time while the recording is not running.
|
||||
public var elapsed: TimeInterval
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates the dynamic content of the Live Activity of a recording flow.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - state: The current state of the recording flow.
|
||||
/// - anchor: The instant the elapsed recording time counts from, or `nil` while the recording is not running.
|
||||
/// - elapsed: The number of seconds spent recording up to the last transition, excluding any time spent paused.
|
||||
public init(
|
||||
state: RecordingActivityState,
|
||||
anchor: Date?,
|
||||
elapsed: TimeInterval
|
||||
) {
|
||||
self.state = state
|
||||
self.anchor = anchor
|
||||
self.elapsed = elapsed
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,7 @@
|
||||
/// A command that controls the recording flow from outside the feature's own UI — for example, from the buttons of a Live Activity.
|
||||
public enum RecordingCommand: Equatable, Sendable {
|
||||
/// Starts, pauses, or resumes the recording, like a press of the feature's main button.
|
||||
case toggle
|
||||
/// Discards the recording, like a press of the feature's discard button.
|
||||
case discard
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
|
||||
/// The bridge that carries ``RecordingCommand``s from outside the feature's UI into the recording flow.
|
||||
///
|
||||
/// The commander exists because the senders of the commands — the App Intents behind a Live Activity's buttons — are instantiated
|
||||
/// by the system, out of reach of the feature's dependency injection: they send through the process-wide ``shared`` instance, and the
|
||||
/// feature's model listens to ``commands``, translating every received command into the button press it mirrors. Unit tests create
|
||||
/// commanders of their own and inject them into the model, leaving the shared instance untouched.
|
||||
@MainActor
|
||||
public final class RecordingCommander {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/// The process-wide commander the recording flow listens to by default.
|
||||
public static let shared = RecordingCommander()
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The stream of commands sent through the commander.
|
||||
public let commands: AsyncStream<RecordingCommand>
|
||||
|
||||
/// The continuation that feeds ``commands``.
|
||||
private let continuation: AsyncStream<RecordingCommand>.Continuation
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates a commander with an empty stream of commands.
|
||||
public init() {
|
||||
(commands, continuation) = AsyncStream.makeStream(of: RecordingCommand.self)
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Sends a command to the recording flow listening to the commander.
|
||||
///
|
||||
/// - Parameter command: The command to send.
|
||||
public func send(
|
||||
_ command: RecordingCommand
|
||||
) {
|
||||
continuation.yield(command)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#if os(iOS)
|
||||
/// The states of the recording flow a Live Activity can show.
|
||||
public enum RecordingActivityState: String, Codable, Hashable, Sendable {
|
||||
/// A recording is in progress and its timer is running.
|
||||
case recording
|
||||
/// The recording is paused.
|
||||
case paused
|
||||
/// The recorded input is being processed.
|
||||
case processing
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
import Testing
|
||||
|
||||
@testable import Commanding
|
||||
|
||||
@Suite("Recording commander")
|
||||
struct RecordingCommanderTests {
|
||||
|
||||
@MainActor
|
||||
@Test
|
||||
func `delivers the sent commands in order`() async throws {
|
||||
let commander = RecordingCommander()
|
||||
|
||||
commander.send(.toggle)
|
||||
commander.send(.discard)
|
||||
commander.send(.toggle)
|
||||
|
||||
var received: [RecordingCommand] = []
|
||||
|
||||
for await command in commander.commands {
|
||||
received.append(command)
|
||||
|
||||
if received.count == 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
#expect(received == [.toggle, .discard, .toggle])
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,6 +12,13 @@
|
||||
|
||||
},
|
||||
"testTargets" : [
|
||||
{
|
||||
"target" : {
|
||||
"containerPath" : "container:",
|
||||
"identifier" : "CommandingTests",
|
||||
"name" : "CommandingTests"
|
||||
}
|
||||
},
|
||||
{
|
||||
"target" : {
|
||||
"containerPath" : "container:",
|
||||
|
||||
Reference in New Issue
Block a user