Skip to content

Commit 014c939

Browse files
committed
Implement typed AsyncSequenceAtom
1 parent 6cacfc2 commit 014c939

File tree

11 files changed

+377
-7
lines changed

11 files changed

+377
-7
lines changed

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,13 +514,52 @@ struct MoviesView: View {
514514

515515
</details>
516516

517-
#### [AsyncThrowingSequenceAtom](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/AsyncThrowingSequenceAtom)
517+
#### [AsyncSequenceAtom](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/asyncsequenceatom)
518+
519+
| |Description|
520+
|:----------|:----------|
521+
|Summary |Provides a `AsyncPhase` value that represents asynchronous, sequential elements of the given `AsyncSequence`.|
522+
|Output |`AsyncPhase<T, E: Error>`|
523+
|Use Case |Handle multiple asynchronous values e.g. web-sockets|
524+
|Available |macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0|
525+
526+
<details><summary><code>📖 Example</code></summary>
527+
528+
```swift
529+
struct NotificationAtom: AsyncSequenceAtom, Hashable {
530+
let name: Notification.Name
531+
532+
func sequence(context: Context) -> NotificationCenter.Notifications {
533+
NotificationCenter.default.notifications(named: name)
534+
}
535+
}
536+
537+
struct NotificationView: View {
538+
@Watch(NotificationAtom(name: UIApplication.didBecomeActiveNotification))
539+
var notificationPhase
540+
541+
var body: some View {
542+
switch notificationPhase {
543+
case .suspending, .failure:
544+
Text("Unknown")
545+
546+
case .success:
547+
Text("Active")
548+
}
549+
}
550+
}
551+
```
552+
553+
</details>
554+
555+
#### [AsyncThrowingSequenceAtom](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/asyncthrowingsequenceatom)
518556

519557
| |Description|
520558
|:----------|:----------|
521559
|Summary |Provides a `AsyncPhase` value that represents asynchronous, sequential elements of the given `AsyncSequence`.|
522560
|Output |`AsyncPhase<T, Error>`|
523561
|Use Case |Handle multiple asynchronous values e.g. web-sockets|
562+
|Deprecated |macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0|
524563

525564
<details><summary><code>📖 Example</code></summary>
526565

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#if compiler(>=6)
2+
/// An atom type that provides asynchronous, sequential elements of the given `AsyncSequence`
3+
/// as an ``AsyncPhase`` value.
4+
///
5+
/// The sequential elements emitted by the `AsyncSequence` will be converted into an enum representation
6+
/// ``AsyncPhase`` that changes overtime. When the sequence emits new elements, it notifies changes to
7+
/// downstream atoms and views, so that they can consume it without suspension points which spawn with
8+
/// `await` keyword.
9+
///
10+
/// ## Output Value
11+
///
12+
/// ``AsyncPhase``<Self.Sequence.Element, Self.Sequence.Failure>
13+
///
14+
/// ## Example
15+
///
16+
/// ```swift
17+
/// struct QuakeMonitorAtom: AsyncSequenceAtom, Hashable {
18+
/// func sequence(context: Context) -> AsyncThrowingStream<Quake, QuakeMonitorError> {
19+
/// AsyncStream { continuation in
20+
/// let monitor = QuakeMonitor()
21+
/// monitor.quakeHandler = { result in
22+
/// continuation.yield(with: result)
23+
/// }
24+
/// continuation.onTermination = { @Sendable _ in
25+
/// monitor.stopMonitoring()
26+
/// }
27+
/// monitor.startMonitoring()
28+
/// }
29+
/// }
30+
/// }
31+
///
32+
/// struct QuakeMonitorView: View {
33+
/// @Watch(QuakeMonitorAtom())
34+
/// var quakes
35+
///
36+
/// var body: some View {
37+
/// switch quakes {
38+
/// case .suspending, .failure:
39+
/// Text("Calm")
40+
///
41+
/// case .failure(let error):
42+
/// Text("Failed: \(error.localizedDescription)")
43+
///
44+
/// case .success(let quake):
45+
/// Text("Quake: \(quake.date)")
46+
/// }
47+
/// }
48+
/// }
49+
/// ```
50+
///
51+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
52+
public protocol AsyncSequenceAtom: AsyncAtom where Produced == AsyncPhase<Sequence.Element, Sequence.Failure> {
53+
/// The type of asynchronous sequence that this atom manages.
54+
associatedtype Sequence: AsyncSequence where Sequence.Element: Sendable
55+
56+
/// Creates an asynchronous sequence to be started when this atom is actually used.
57+
///
58+
/// The sequence that is produced by this method must be instantiated anew each time this method
59+
/// is called. Otherwise, it could throw a fatal error because Swift Concurrency doesn't allow
60+
/// single `AsyncSequence` instance to be shared between multiple subscriptions.
61+
///
62+
/// - Parameter context: A context structure to read, watch, and otherwise
63+
/// interact with other atoms.
64+
///
65+
/// - Returns: An asynchronous sequence that produces asynchronous, sequential elements.
66+
@MainActor
67+
func sequence(context: Context) -> Sequence
68+
}
69+
70+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
71+
public extension AsyncSequenceAtom {
72+
var producer: AtomProducer<Produced> {
73+
AtomProducer { context in
74+
let sequence = context.transaction(sequence)
75+
let task = Task {
76+
do throws(Sequence.Failure) {
77+
for try await element in sequence {
78+
if !Task.isCancelled {
79+
context.update(with: .success(element))
80+
}
81+
}
82+
}
83+
catch {
84+
if !Task.isCancelled {
85+
context.update(with: .failure(error))
86+
}
87+
}
88+
}
89+
90+
context.onTermination = task.cancel
91+
return .suspending
92+
}
93+
}
94+
95+
var refreshProducer: AtomRefreshProducer<Produced> {
96+
AtomRefreshProducer { context in
97+
let sequence = context.transaction(sequence)
98+
let task = Task {
99+
var phase = Produced.suspending
100+
101+
do throws(Sequence.Failure) {
102+
for try await element in sequence {
103+
if !Task.isCancelled {
104+
phase = .success(element)
105+
}
106+
}
107+
}
108+
catch {
109+
if !Task.isCancelled {
110+
phase = .failure(error)
111+
}
112+
}
113+
114+
return phase
115+
}
116+
117+
context.onTermination = task.cancel
118+
119+
return await withTaskCancellationHandler {
120+
await task.value
121+
} onCancel: {
122+
task.cancel()
123+
}
124+
}
125+
}
126+
}
127+
#else
128+
/// Deprecated.
129+
///
130+
/// - SeeAlso: ``AsyncThrowingSequenceAtom``
131+
@available(*, deprecated, renamed: "AsyncThrowingSequenceAtom")
132+
public typealias AsyncSequenceAtom = AsyncThrowingSequenceAtom
133+
#endif

Sources/Atoms/Atom/AsyncThrowingSequenceAtom.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@
4444
/// }
4545
/// ```
4646
///
47+
#if compiler(>=6)
48+
@available(macOS, deprecated: 15.0, message: "AsyncThrowingSequenceAtom has been replaced by AsyncSequenceAtom") // swift-format-ignore
49+
@available(iOS, deprecated: 18.0, message: "AsyncThrowingSequenceAtom has been replaced by AsyncSequenceAtom") // swift-format-ignore
50+
@available(watchOS, deprecated: 11.0, message: "AsyncThrowingSequenceAtom has been replaced by AsyncSequenceAtom") // swift-format-ignore
51+
@available(tvOS, deprecated: 18.0, message: "AsyncThrowingSequenceAtom has been replaced by AsyncSequenceAtom") // swift-format-ignore
52+
@available(visionOS, deprecated: 2.0, message: "AsyncThrowingSequenceAtom has been replaced by AsyncSequenceAtom") // swift-format-ignore
53+
#endif
4754
public protocol AsyncThrowingSequenceAtom: AsyncAtom where Produced == AsyncPhase<Sequence.Element, Error> {
4855
/// The type of asynchronous sequence that this atom manages.
4956
associatedtype Sequence: AsyncSequence where Sequence.Element: Sendable
@@ -52,7 +59,7 @@ public protocol AsyncThrowingSequenceAtom: AsyncAtom where Produced == AsyncPhas
5259
///
5360
/// The sequence that is produced by this method must be instantiated anew each time this method
5461
/// is called. Otherwise, it could throw a fatal error because Swift Concurrency doesn't allow
55-
/// single `AsyncSequence` instance to be shared between multiple locations.
62+
/// single `AsyncSequence` instance to be shared between multiple subscriptions.
5663
///
5764
/// - Parameter context: A context structure to read, watch, and otherwise
5865
/// interact with other atoms.

Sources/Atoms/Atoms.docc/Atoms.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Building state by compositing atoms automatically optimizes rendering based on i
2020
- ``StateAtom``
2121
- ``TaskAtom``
2222
- ``ThrowingTaskAtom``
23+
- ``AsyncSequenceAtom``
2324
- ``AsyncThrowingSequenceAtom``
2425
- ``PublisherAtom``
2526
- ``ObservableObjectAtom``

Sources/Atoms/Context/AtomContext.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public protocol AtomContext {
6565
/// Refreshes and then returns the value associated with the given refreshable atom.
6666
///
6767
/// This method accepts only asynchronous atoms such as types conforming to:
68-
/// ``TaskAtom``, ``ThrowingTaskAtom``, ``AsyncThrowingSequenceAtom``, ``PublisherAtom``.
68+
/// ``TaskAtom``, ``ThrowingTaskAtom``, ``AsyncThrowingSequenceAtom``, ``AsyncSequenceAtom``, or ``PublisherAtom``.
6969
/// It refreshes the value for the given atom and then returns, so the caller can await until
7070
/// the atom completes the update.
7171
/// Note that it can be used only in a context that supports concurrency.

Sources/Atoms/Context/AtomCurrentContext.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public struct AtomCurrentContext: AtomContext {
7676
/// Refreshes and then returns the value associated with the given refreshable atom.
7777
///
7878
/// This method accepts only asynchronous atoms such as types conforming to:
79-
/// ``TaskAtom``, ``ThrowingTaskAtom``, ``AsyncThrowingSequenceAtom``, ``PublisherAtom``.
79+
/// ``TaskAtom``, ``ThrowingTaskAtom``, ``AsyncThrowingSequenceAtom``, ``AsyncSequenceAtom``, or ``PublisherAtom``.
8080
/// It refreshes the value for the given atom and then returns, so the caller can await until
8181
/// the atom completes the update.
8282
/// Note that it can be used only in a context that supports concurrency.

Sources/Atoms/Context/AtomTestContext.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public struct AtomTestContext: AtomWatchableContext {
220220
/// Refreshes and then returns the value associated with the given refreshable atom.
221221
///
222222
/// This method accepts only asynchronous atoms such as types conforming to:
223-
/// ``TaskAtom``, ``ThrowingTaskAtom``, ``AsyncThrowingSequenceAtom``, ``PublisherAtom``.
223+
/// ``TaskAtom``, ``ThrowingTaskAtom``, ``AsyncThrowingSequenceAtom``, ``AsyncSequenceAtom``, or ``PublisherAtom``.
224224
/// It refreshes the value for the given atom and then returns, so the caller can await until
225225
/// the atom completes the update.
226226
/// Note that it can be used only in a context that supports concurrency.

Sources/Atoms/Context/AtomTransactionContext.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public struct AtomTransactionContext: AtomWatchableContext {
8686
/// Refreshes and then returns the value associated with the given refreshable atom.
8787
///
8888
/// This method accepts only asynchronous atoms such as types conforming to:
89-
/// ``TaskAtom``, ``ThrowingTaskAtom``, ``AsyncThrowingSequenceAtom``, ``PublisherAtom``.
89+
/// ``TaskAtom``, ``ThrowingTaskAtom``, ``AsyncThrowingSequenceAtom``, ``AsyncSequenceAtom``, or ``PublisherAtom``.
9090
/// It refreshes the value for the given atom and then returns, so the caller can await until
9191
/// the atom completes the update.
9292
/// Note that it can be used only in a context that supports concurrency.

Sources/Atoms/Context/AtomViewContext.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public struct AtomViewContext: AtomWatchableContext {
9292
/// Refreshes and then returns the value associated with the given refreshable atom.
9393
///
9494
/// This method accepts only asynchronous atoms such as types conforming to:
95-
/// ``TaskAtom``, ``ThrowingTaskAtom``, ``AsyncThrowingSequenceAtom``, ``PublisherAtom``.
95+
/// ``TaskAtom``, ``ThrowingTaskAtom``, ``AsyncThrowingSequenceAtom``, ``AsyncSequenceAtom``, or ``PublisherAtom``.
9696
/// It refreshes the value for the given atom and then returns, so the caller can await until
9797
/// the atom completes the update.
9898
/// Note that it can be used only in a context that supports concurrency.

0 commit comments

Comments
 (0)