Skip to content

Commit 3005f19

Browse files
authored
Add support for resultBuilder syntax for AtomEffect (#183)
* Add AtomEffectBuilder * Update docc md * Add test cases * Use result builder syntax * Deprecate EmptyEffect * Deprecate MergedEffect * Refactoring * Refactoring
1 parent 4efc094 commit 3005f19

File tree

8 files changed

+465
-11
lines changed

8 files changed

+465
-11
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,15 +1399,14 @@ Note that other atoms that depend on scoped atoms will be in a shared state and
13991399

14001400
Atom effects are an API for managing side effects that are synchronized with the atom's lifecycle. They are widely applicable for variety of usage such as state synchronization, state persistence, logging, and etc, by observing and reacting to state changes.
14011401

1402-
You can create custom effects that conform to the [`AtomEffect`](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomeffect) protocol, but there are several predefined effects.
1402+
You can create custom effects that conform to the [`AtomEffect`](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomeffect) protocol, but there are several predefined effects. Multiple atom effects can be specified with the result builder syntax.
14031403

14041404
|API|Use|
14051405
|:--|:--|
14061406
|[InitializingEffect](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/initializingeffect)|Performs an arbitrary action before the atom is initialized.|
14071407
|[InitializeEffect](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/initializeeffect)|Performs an arbitrary action after the atom is initialized.|
14081408
|[UpdateEffect](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/updateeffect)|Performs an arbitrary action when the atom is updated.|
14091409
|[ReleaseEffect](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/releaseeffect)|Performs an arbitrary action when the atom is released.|
1410-
|[MergedEffect](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/mergedeffect)|Merges multiple atom effects into one.|
14111410

14121411
Atom effects are attached to atoms via the [`Atom.effect(context:)`](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/effect(context:)-4wm5m) function.
14131412

Sources/Atoms/Atoms.docc/Atoms.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ Building state by compositing atoms automatically optimizes rendering based on i
3737
### Effects
3838

3939
- ``AtomEffect``
40+
- ``AtomEffectBuilder``
4041
- ``InitializingEffect``
4142
- ``InitializeEffect``
4243
- ``UpdateEffect``
4344
- ``ReleaseEffect``
44-
- ``MergedEffect``
4545

4646
### Attributes
4747

@@ -90,6 +90,10 @@ Building state by compositing atoms automatically optimizes rendering based on i
9090
- ``ChangesOfModifier``
9191
- ``TaskPhaseModifier``
9292
- ``AnimationModifier``
93-
- ``EmptyEffect``
9493
- ``AtomProducer``
9594
- ``AtomRefreshProducer``
95+
96+
### Deprecated
97+
98+
- ``EmptyEffect``
99+
- ``MergedEffect``

Sources/Atoms/Core/Atom/Atom.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public protocol Atom<Produced>: Sendable {
1010
associatedtype Produced
1111

1212
/// The type of effect for managing side effects.
13-
associatedtype Effect: AtomEffect = EmptyEffect
13+
associatedtype Effect: AtomEffect
1414

1515
/// A type of the context structure to read, watch, and otherwise interact
1616
/// with other atoms.
@@ -34,6 +34,7 @@ public protocol Atom<Produced>: Sendable {
3434
///
3535
/// - Returns: An effect for managing side effects.
3636
@MainActor
37+
@AtomEffectBuilder
3738
func effect(context: CurrentContext) -> Effect
3839

3940
// --- Internal ---
@@ -44,9 +45,8 @@ public protocol Atom<Produced>: Sendable {
4445

4546
public extension Atom {
4647
@MainActor
47-
func effect(context: CurrentContext) -> Effect where Effect == EmptyEffect {
48-
EmptyEffect()
49-
}
48+
@AtomEffectBuilder
49+
func effect(context: CurrentContext) -> some AtomEffect {}
5050
}
5151

5252
public extension Atom where Self == Key {

Sources/Atoms/Core/Effect/EmptyEffect.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/// An effect that doesn't produce any effects.
2+
@available(*, deprecated, message: "`Atom/effect(context:)` now supports result builder syntax.")
23
public struct EmptyEffect: AtomEffect {
34
/// Creates an empty effect.
45
public init() {}

Sources/Atoms/Effect/MergedEffect.swift renamed to Sources/Atoms/Core/Effect/MergedEffect.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
// Use type pack once it is available in iOS 17 or newer.
2-
// MergedEffect<each Effect: AtomEffect>
31
/// An atom effect that merges multiple atom effects into one.
2+
@available(*, deprecated, message: "`Atom/effect(context:)` now supports result builder syntax.")
43
public struct MergedEffect: AtomEffect {
54
private let initializing: @MainActor (Context) -> Void
65
private let initialized: @MainActor (Context) -> Void

Sources/Atoms/Effect/AtomEffect.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
/// SeeAlso: ``InitializeEffect``
99
/// SeeAlso: ``UpdateEffect``
1010
/// SeeAlso: ``ReleaseEffect``
11-
/// SeeAlso: ``MergedEffect``
1211
@MainActor
1312
public protocol AtomEffect {
1413
/// A type of the context structure to read, set, and otherwise interact
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
2+
/// A result builder for composing multiple atom effects into a single effect.
3+
///
4+
/// ## Example
5+
/// ```swift
6+
/// @AtomEffectBuilder
7+
/// func effect(context: CurrentContext) -> some AtomEffect {
8+
/// UpdateEffect {
9+
/// print("Updated")
10+
/// }
11+
///
12+
/// ReleaseEffect {
13+
/// print("Released")
14+
/// }
15+
///
16+
/// CustomEffect()
17+
/// }
18+
/// ```
19+
@MainActor
20+
@resultBuilder
21+
public enum AtomEffectBuilder {
22+
public static func buildBlock<Effect: AtomEffect>(_ effect: Effect) -> Effect {
23+
effect
24+
}
25+
26+
public static func buildBlock<each Effect: AtomEffect>(_ effect: repeat each Effect) -> some AtomEffect {
27+
if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) {
28+
return BlockEffect(repeat each effect)
29+
}
30+
else {
31+
return BlockBCEffect(repeat each effect)
32+
}
33+
}
34+
35+
public static func buildIf<Effect: AtomEffect>(_ effect: Effect?) -> some AtomEffect {
36+
IfEffect(effect)
37+
}
38+
39+
public static func buildEither<TrueEffect: AtomEffect, FalseEffect: AtomEffect>(
40+
first: TrueEffect
41+
) -> ConditionalEffect<TrueEffect, FalseEffect> {
42+
ConditionalEffect(storage: .trueEffect(first))
43+
}
44+
45+
public static func buildEither<TrueEffect: AtomEffect, FalseEffect: AtomEffect>(
46+
second: FalseEffect
47+
) -> ConditionalEffect<TrueEffect, FalseEffect> {
48+
ConditionalEffect(storage: .falseEffect(second))
49+
}
50+
51+
public static func buildLimitedAvailability(_ effect: any AtomEffect) -> some AtomEffect {
52+
LimitedAvailabilityEffect(effect)
53+
}
54+
}
55+
56+
public extension AtomEffectBuilder {
57+
struct ConditionalEffect<TrueEffect: AtomEffect, FalseEffect: AtomEffect>: AtomEffect {
58+
internal enum Storage {
59+
case trueEffect(TrueEffect)
60+
case falseEffect(FalseEffect)
61+
}
62+
63+
private let storage: Storage
64+
65+
internal init(storage: Storage) {
66+
self.storage = storage
67+
}
68+
69+
public func initializing(context: Context) {
70+
switch storage {
71+
case .trueEffect(let trueEffect):
72+
trueEffect.initializing(context: context)
73+
74+
case .falseEffect(let falseEffect):
75+
falseEffect.initializing(context: context)
76+
}
77+
}
78+
79+
public func initialized(context: Context) {
80+
switch storage {
81+
case .trueEffect(let trueEffect):
82+
trueEffect.initialized(context: context)
83+
84+
case .falseEffect(let falseEffect):
85+
falseEffect.initialized(context: context)
86+
}
87+
}
88+
89+
public func updated(context: Context) {
90+
switch storage {
91+
case .trueEffect(let trueEffect):
92+
trueEffect.updated(context: context)
93+
94+
case .falseEffect(let falseEffect):
95+
falseEffect.updated(context: context)
96+
}
97+
}
98+
99+
public func released(context: Context) {
100+
switch storage {
101+
case .trueEffect(let trueEffect):
102+
trueEffect.released(context: context)
103+
case .falseEffect(let falseEffect):
104+
falseEffect.released(context: context)
105+
}
106+
}
107+
}
108+
}
109+
110+
private extension AtomEffectBuilder {
111+
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
112+
struct BlockEffect<each Effect: AtomEffect>: AtomEffect {
113+
private let effect: (repeat each Effect)
114+
115+
init(_ effect: repeat each Effect) {
116+
self.effect = (repeat each effect)
117+
}
118+
119+
func initializing(context: Context) {
120+
repeat (each effect).initializing(context: context)
121+
}
122+
123+
func initialized(context: Context) {
124+
repeat (each effect).initialized(context: context)
125+
}
126+
127+
func updated(context: Context) {
128+
repeat (each effect).updated(context: context)
129+
}
130+
131+
func released(context: Context) {
132+
repeat (each effect).released(context: context)
133+
}
134+
}
135+
136+
struct BlockBCEffect: AtomEffect {
137+
private let _initializing: @MainActor (Context) -> Void
138+
private let _initialized: @MainActor (Context) -> Void
139+
private let _updated: @MainActor (Context) -> Void
140+
private let _released: @MainActor (Context) -> Void
141+
142+
init<each Effect: AtomEffect>(_ effect: repeat each Effect) {
143+
_initializing = { context in
144+
repeat (each effect).initializing(context: context)
145+
}
146+
_initialized = { context in
147+
repeat (each effect).initialized(context: context)
148+
}
149+
_updated = { context in
150+
repeat (each effect).updated(context: context)
151+
}
152+
_released = { context in
153+
repeat (each effect).released(context: context)
154+
}
155+
}
156+
157+
func initializing(context: Context) {
158+
_initializing(context)
159+
}
160+
161+
func initialized(context: Context) {
162+
_initialized(context)
163+
}
164+
165+
func updated(context: Context) {
166+
_updated(context)
167+
}
168+
169+
func released(context: Context) {
170+
_released(context)
171+
}
172+
}
173+
174+
struct IfEffect<Effect: AtomEffect>: AtomEffect {
175+
private let effect: Effect?
176+
177+
init(_ effect: Effect?) {
178+
self.effect = effect
179+
}
180+
181+
func initializing(context: Context) {
182+
effect?.initializing(context: context)
183+
}
184+
185+
func initialized(context: Context) {
186+
effect?.initialized(context: context)
187+
}
188+
189+
func updated(context: Context) {
190+
effect?.updated(context: context)
191+
}
192+
193+
func released(context: Context) {
194+
effect?.released(context: context)
195+
}
196+
}
197+
198+
struct LimitedAvailabilityEffect: AtomEffect {
199+
private let effect: any AtomEffect
200+
201+
init(_ effect: any AtomEffect) {
202+
self.effect = effect
203+
}
204+
205+
func initializing(context: Context) {
206+
effect.initializing(context: context)
207+
}
208+
209+
func initialized(context: Context) {
210+
effect.initialized(context: context)
211+
}
212+
213+
func updated(context: Context) {
214+
effect.updated(context: context)
215+
}
216+
217+
func released(context: Context) {
218+
effect.released(context: context)
219+
}
220+
}
221+
}

0 commit comments

Comments
 (0)