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: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1403,7 +1403,8 @@ You can create custom effects that conform to the [`AtomEffect`](https://ra1028.

|API|Use|
|:--|:--|
|[InitializeEffect](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/initializeeffect)|Performs an arbitrary action when the atom is initialized.|
|[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.|
Expand Down
1 change: 1 addition & 0 deletions Sources/Atoms/Atoms.docc/Atoms.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Building state by compositing atoms automatically optimizes rendering based on i
### Effects

- ``AtomEffect``
- ``InitializingEffect``
- ``InitializeEffect``
- ``UpdateEffect``
- ``ReleaseEffect``
Expand Down
7 changes: 4 additions & 3 deletions Sources/Atoms/Core/StoreContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -279,14 +279,15 @@ private extension StoreContext {
for key: AtomKey,
override: Override<Node>?
) -> Node.Produced {
let value = getValue(of: atom, for: key, override: override)
let state = getState(of: atom, for: key)
let currentContext = AtomCurrentContext(store: self)

state.effect.initializing(context: currentContext)
Copy link

Copilot AI May 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding an inline comment here to clarify that the initializing hook is intentionally invoked before retrieving the atom's value to capture pre-initialization state.

Copilot uses AI. Check for mistakes.

let value = getValue(of: atom, for: key, override: override)
store.state.caches[key] = AtomCache(atom: atom, value: value, initializedScope: currentScope)

let currentContext = AtomCurrentContext(store: self)
state.effect.initialized(context: currentContext)

return value
}

Expand Down
10 changes: 8 additions & 2 deletions Sources/Atoms/Effect/AtomEffect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/// initialized the first time the atom is used, and the instance will be retained
/// until the atom is released, thus it allows to declare stateful side effects.
///
/// SeeAlso: ``InitializingEffect``
/// SeeAlso: ``InitializeEffect``
/// SeeAlso: ``UpdateEffect``
/// SeeAlso: ``ReleaseEffect``
Expand All @@ -14,8 +15,12 @@ public protocol AtomEffect {
/// with other atoms.
typealias Context = AtomCurrentContext

/// A lifecycle event that is triggered when the atom is first used and initialized,
/// or once it is released and re-initialized again.
/// A lifecycle event that is triggered before the atom is first used and initialized,
/// or once it is released and re-initialized.
func initializing(context: Context)

/// A lifecycle event that is triggered after the atom is first used and initialized,
/// or once it is released and re-initialized.
func initialized(context: Context)

/// A lifecycle event that is triggered when the atom is updated.
Expand All @@ -26,6 +31,7 @@ public protocol AtomEffect {
}

public extension AtomEffect {
func initializing(context: Context) {}
func initialized(context: Context) {}
func updated(context: Context) {}
func released(context: Context) {}
Expand Down
10 changes: 5 additions & 5 deletions Sources/Atoms/Effect/InitializeEffect.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
/// An atom effect that performs an arbitrary action when the atom is first used and initialized,
/// or once it is released and re-initialized again.
/// An atom effect that performs an arbitrary action after the atom is first used and initialized,
/// or once it is released and re-initialized.
public struct InitializeEffect: AtomEffect {
private let action: @MainActor () -> Void

/// Creates an atom effect that performs the given action when the atom is initialized.
/// Creates an atom effect that performs the given action after the atom is initialized.
public init(perform action: @MainActor @escaping () -> Void) {
self.action = action
}

/// A lifecycle event that is triggered when the atom is first used and initialized,
/// or once it is released and re-initialized again.
/// A lifecycle event that is triggered after the atom is first used and initialized,
/// or once it is released and re-initialized.
public func initialized(context: Context) {
action()
}
Expand Down
16 changes: 16 additions & 0 deletions Sources/Atoms/Effect/InitializingEffect.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/// An atom effect that performs an arbitrary action before the atom is first used and initialized,
/// or once it is released and re-initialized.
public struct InitializingEffect: AtomEffect {
private let action: @MainActor () -> Void

/// Creates an atom effect that performs the given action before the atom is initialized.
public init(perform action: @MainActor @escaping () -> Void) {
self.action = action
}

/// A lifecycle event that is triggered before the atom is first used and initialized,
/// or once it is released and re-initialized.
public func initializing(context: Context) {
action()
}
}
6 changes: 6 additions & 0 deletions Tests/AtomsTests/Core/StoreContextTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1157,13 +1157,15 @@ final class StoreContextTests: XCTestCase {
_ = context.watch(upstreamAtom, in: TransactionState(key: key))

XCTAssertTrue((store.state.states[key]?.effect as? TestEffect) === effect)
XCTAssertEqual(effect.initializingCount, 1)
XCTAssertEqual(effect.initializedCount, 1)
XCTAssertEqual(effect.updatedCount, 0)
XCTAssertEqual(effect.releasedCount, 0)

context.set(1, for: atom)

XCTAssertTrue((store.state.states[key]?.effect as? TestEffect) === effect)
XCTAssertEqual(effect.initializingCount, 1)
XCTAssertEqual(effect.initializedCount, 1)
XCTAssertEqual(effect.updatedCount, 1)
XCTAssertEqual(effect.releasedCount, 0)
Expand All @@ -1173,27 +1175,31 @@ final class StoreContextTests: XCTestCase {
context.set(4, for: atom)

XCTAssertTrue((store.state.states[key]?.effect as? TestEffect) === effect)
XCTAssertEqual(effect.initializingCount, 1)
XCTAssertEqual(effect.initializedCount, 1)
XCTAssertEqual(effect.updatedCount, 4)
XCTAssertEqual(effect.releasedCount, 0)

context.set("Updated", for: upstreamAtom)

XCTAssertTrue((store.state.states[key]?.effect as? TestEffect) === effect)
XCTAssertEqual(effect.initializingCount, 1)
XCTAssertEqual(effect.initializedCount, 1)
XCTAssertEqual(effect.updatedCount, 5)
XCTAssertEqual(effect.releasedCount, 0)

context.unwatch(atom, subscriber: subscriber)

XCTAssertNil(store.state.states[key])
XCTAssertEqual(effect.initializingCount, 1)
XCTAssertEqual(effect.initializedCount, 1)
XCTAssertEqual(effect.updatedCount, 5)
XCTAssertEqual(effect.releasedCount, 1)

context.set(5, for: atom)

XCTAssertNil(store.state.states[key])
XCTAssertEqual(effect.initializingCount, 1)
XCTAssertEqual(effect.initializedCount, 1)
XCTAssertEqual(effect.updatedCount, 5)
XCTAssertEqual(effect.releasedCount, 1)
Expand Down
5 changes: 4 additions & 1 deletion Tests/AtomsTests/Effect/InitializeEffectTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import XCTest

@testable import Atoms

final class InitializeTests: XCTestCase {
final class InitializeEffectTests: XCTestCase {
@MainActor
func testEvent() {
let context = AtomCurrentContext(store: .dummy)
Expand All @@ -11,6 +11,9 @@ final class InitializeTests: XCTestCase {
performedCount += 1
}

effect.initializing(context: context)
XCTAssertEqual(performedCount, 0)

effect.initialized(context: context)
XCTAssertEqual(performedCount, 1)

Expand Down
26 changes: 26 additions & 0 deletions Tests/AtomsTests/Effect/InitializingEffectTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import XCTest

@testable import Atoms

final class InitializingEffectTests: XCTestCase {
@MainActor
func testEvent() {
let context = AtomCurrentContext(store: .dummy)
var performedCount = 0
let effect = InitializingEffect {
performedCount += 1
}

effect.initializing(context: context)
XCTAssertEqual(performedCount, 1)

effect.initialized(context: context)
XCTAssertEqual(performedCount, 1)

effect.updated(context: context)
XCTAssertEqual(performedCount, 1)

effect.released(context: context)
XCTAssertEqual(performedCount, 1)
}
}
3 changes: 3 additions & 0 deletions Tests/AtomsTests/Effect/ReleaseEffectTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ final class ReleaseEffectTests: XCTestCase {
performedCount += 1
}

effect.initializing(context: context)
Copy link

Copilot AI May 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider adding a comment here to clarify why invoking initializing on release effects should not affect performedCount, aiding future test maintenance.

Copilot uses AI. Check for mistakes.
XCTAssertEqual(performedCount, 0)

effect.initialized(context: context)
XCTAssertEqual(performedCount, 0)

Expand Down
3 changes: 3 additions & 0 deletions Tests/AtomsTests/Effect/UpdateEffectTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ final class UpdateEffectTests: XCTestCase {
performedCount += 1
}

effect.initializing(context: context)
Copy link

Copilot AI May 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] It may help to add an inline comment explaining that the initializing hook is expected to be a no-op for update effects, which improves clarity of the test intent.

Copilot uses AI. Check for mistakes.
XCTAssertEqual(performedCount, 0)

effect.initialized(context: context)
XCTAssertEqual(performedCount, 0)

Expand Down
5 changes: 5 additions & 0 deletions Tests/AtomsTests/Utilities/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ final class Object {}
struct UniqueKey: Hashable {}

final class TestEffect: AtomEffect {
var initializingCount = 0
var initializedCount = 0
var updatedCount = 0
var releasedCount = 0

func initializing(context: Context) {
initializingCount += 1
}

func initialized(context: Context) {
initializedCount += 1
}
Expand Down