Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1399,15 +1399,14 @@ Note that other atoms that depend on scoped atoms will be in a shared state and

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.

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.
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.

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

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.

Expand Down
8 changes: 6 additions & 2 deletions Sources/Atoms/Atoms.docc/Atoms.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ Building state by compositing atoms automatically optimizes rendering based on i
### Effects

- ``AtomEffect``
- ``AtomEffectBuilder``
- ``InitializingEffect``
- ``InitializeEffect``
- ``UpdateEffect``
- ``ReleaseEffect``
- ``MergedEffect``

### Attributes

Expand Down Expand Up @@ -90,6 +90,10 @@ Building state by compositing atoms automatically optimizes rendering based on i
- ``ChangesOfModifier``
- ``TaskPhaseModifier``
- ``AnimationModifier``
- ``EmptyEffect``
- ``AtomProducer``
- ``AtomRefreshProducer``

### Deprecated

- ``EmptyEffect``
- ``MergedEffect``
8 changes: 4 additions & 4 deletions Sources/Atoms/Core/Atom/Atom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public protocol Atom<Produced>: Sendable {
associatedtype Produced

/// The type of effect for managing side effects.
associatedtype Effect: AtomEffect = EmptyEffect
associatedtype Effect: AtomEffect

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

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

public extension Atom {
@MainActor
func effect(context: CurrentContext) -> Effect where Effect == EmptyEffect {
EmptyEffect()
}
@AtomEffectBuilder
func effect(context: CurrentContext) -> some AtomEffect {}
}

public extension Atom where Self == Key {
Expand Down
1 change: 1 addition & 0 deletions Sources/Atoms/Core/Effect/EmptyEffect.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/// An effect that doesn't produce any effects.
@available(*, deprecated, message: "`Atom/effect(context:)` now supports result builder syntax.")
public struct EmptyEffect: AtomEffect {
/// Creates an empty effect.
public init() {}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Use type pack once it is available in iOS 17 or newer.
// MergedEffect<each Effect: AtomEffect>
/// An atom effect that merges multiple atom effects into one.
@available(*, deprecated, message: "`Atom/effect(context:)` now supports result builder syntax.")
public struct MergedEffect: AtomEffect {
private let initializing: @MainActor (Context) -> Void
private let initialized: @MainActor (Context) -> Void
Expand Down
1 change: 0 additions & 1 deletion Sources/Atoms/Effect/AtomEffect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
/// SeeAlso: ``InitializeEffect``
/// SeeAlso: ``UpdateEffect``
/// SeeAlso: ``ReleaseEffect``
/// SeeAlso: ``MergedEffect``
@MainActor
public protocol AtomEffect {
/// A type of the context structure to read, set, and otherwise interact
Expand Down
221 changes: 221 additions & 0 deletions Sources/Atoms/Effect/AtomEffectBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
/// A result builder for composing multiple atom effects into a single effect.
///
/// ## Example
/// ```swift
/// @AtomEffectBuilder
/// func effect(context: CurrentContext) -> some AtomEffect {
/// UpdateEffect {
/// print("Updated")
/// }
///
/// ReleaseEffect {
/// print("Released")
/// }
///
/// CustomEffect()
/// }
/// ```
@MainActor
@resultBuilder
public enum AtomEffectBuilder {
public static func buildBlock<Effect: AtomEffect>(_ effect: Effect) -> Effect {
effect
}

public static func buildBlock<each Effect: AtomEffect>(_ effect: repeat each Effect) -> some AtomEffect {
if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) {
return BlockEffect(repeat each effect)
}
else {
return BlockBCEffect(repeat each effect)
}
}

public static func buildIf<Effect: AtomEffect>(_ effect: Effect?) -> some AtomEffect {
IfEffect(effect)
}

public static func buildEither<TrueEffect: AtomEffect, FalseEffect: AtomEffect>(
first: TrueEffect
) -> ConditionalEffect<TrueEffect, FalseEffect> {
ConditionalEffect(storage: .trueEffect(first))
}

public static func buildEither<TrueEffect: AtomEffect, FalseEffect: AtomEffect>(
second: FalseEffect
) -> ConditionalEffect<TrueEffect, FalseEffect> {
ConditionalEffect(storage: .falseEffect(second))
}

public static func buildLimitedAvailability(_ effect: any AtomEffect) -> some AtomEffect {
LimitedAvailabilityEffect(effect)
}
}

public extension AtomEffectBuilder {
struct ConditionalEffect<TrueEffect: AtomEffect, FalseEffect: AtomEffect>: AtomEffect {
internal enum Storage {
case trueEffect(TrueEffect)
case falseEffect(FalseEffect)
}

private let storage: Storage

internal init(storage: Storage) {
self.storage = storage
}

public func initializing(context: Context) {
switch storage {
case .trueEffect(let trueEffect):
trueEffect.initializing(context: context)

case .falseEffect(let falseEffect):
falseEffect.initializing(context: context)
}
}

public func initialized(context: Context) {
switch storage {
case .trueEffect(let trueEffect):
trueEffect.initialized(context: context)

case .falseEffect(let falseEffect):
falseEffect.initialized(context: context)
}
}

public func updated(context: Context) {
switch storage {
case .trueEffect(let trueEffect):
trueEffect.updated(context: context)

case .falseEffect(let falseEffect):
falseEffect.updated(context: context)
}
}

public func released(context: Context) {
switch storage {
case .trueEffect(let trueEffect):
trueEffect.released(context: context)
case .falseEffect(let falseEffect):
falseEffect.released(context: context)
}
}
}
}

private extension AtomEffectBuilder {
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
struct BlockEffect<each Effect: AtomEffect>: AtomEffect {
private let effect: (repeat each Effect)

init(_ effect: repeat each Effect) {
self.effect = (repeat each effect)
}

func initializing(context: Context) {
repeat (each effect).initializing(context: context)
}

func initialized(context: Context) {
repeat (each effect).initialized(context: context)
}

func updated(context: Context) {
repeat (each effect).updated(context: context)
}

func released(context: Context) {
repeat (each effect).released(context: context)
}
}

struct BlockBCEffect: AtomEffect {
private let _initializing: @MainActor (Context) -> Void
private let _initialized: @MainActor (Context) -> Void
private let _updated: @MainActor (Context) -> Void
private let _released: @MainActor (Context) -> Void

init<each Effect: AtomEffect>(_ effect: repeat each Effect) {
_initializing = { context in
repeat (each effect).initializing(context: context)
}
_initialized = { context in
repeat (each effect).initialized(context: context)
}
_updated = { context in
repeat (each effect).updated(context: context)
}
_released = { context in
repeat (each effect).released(context: context)
}
}

func initializing(context: Context) {
_initializing(context)
}

func initialized(context: Context) {
_initialized(context)
}

func updated(context: Context) {
_updated(context)
}

func released(context: Context) {
_released(context)
}
}

struct IfEffect<Effect: AtomEffect>: AtomEffect {
private let effect: Effect?

init(_ effect: Effect?) {
self.effect = effect
}

func initializing(context: Context) {
effect?.initializing(context: context)
}

func initialized(context: Context) {
effect?.initialized(context: context)
}

func updated(context: Context) {
effect?.updated(context: context)
}

func released(context: Context) {
effect?.released(context: context)
}
}

struct LimitedAvailabilityEffect: AtomEffect {
private let effect: any AtomEffect

init(_ effect: any AtomEffect) {
self.effect = effect
}

func initializing(context: Context) {
effect.initializing(context: context)
}

func initialized(context: Context) {
effect.initialized(context: context)
}

func updated(context: Context) {
effect.updated(context: context)
}

func released(context: Context) {
effect.released(context: context)
}
}
}
Loading