From 7641387d52cf1b25c1d8dacb830b522008e2b0a2 Mon Sep 17 00:00:00 2001 From: ra1028 Date: Fri, 3 Oct 2025 17:02:22 +0900 Subject: [PATCH 1/3] Implement previous modifier --- Sources/Atoms/Modifier/PreviousModifier.swift | 75 ++++++++++++++++ .../Modifier/PreviousModifierTests.swift | 87 +++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 Sources/Atoms/Modifier/PreviousModifier.swift create mode 100644 Tests/AtomsTests/Modifier/PreviousModifierTests.swift diff --git a/Sources/Atoms/Modifier/PreviousModifier.swift b/Sources/Atoms/Modifier/PreviousModifier.swift new file mode 100644 index 00000000..f64d9c16 --- /dev/null +++ b/Sources/Atoms/Modifier/PreviousModifier.swift @@ -0,0 +1,75 @@ +public extension Atom { + /// Provides the previous value of the atom instead of the current value. + /// + /// ```swift + /// struct CounterAtom: StateAtom, Hashable { + /// func defaultValue(context: Context) -> Int { + /// 0 + /// } + /// } + /// + /// struct ExampleView: View { + /// @Watch(CounterAtom()) + /// var currentValue + /// + /// @Watch(CounterAtom().previous) + /// var previousValue + /// + /// var body: some View { + /// VStack { + /// Text("Current: \(currentValue)") + /// Text("Previous: \(previousValue ?? 0)") + /// } + /// } + /// } + /// ``` + /// + var previous: ModifiedAtom> { + modifier(PreviousModifier()) + } +} + +/// A modifier that provides the previous value of the atom instead of the current value. +/// +/// Use ``Atom/previous`` instead of using this modifier directly. +public struct PreviousModifier: AtomModifier { + /// A type of base value to be modified. + public typealias Base = Base + + /// A type of value the modified atom produces. + public typealias Produced = Base? + + /// A type representing the stable identity of this atom associated with an instance. + public struct Key: Hashable, Sendable {} + + /// A unique value used to identify the modifier internally. + public var key: Key { + Key() + } + + /// A producer that produces the value of this atom. + public func producer(atom: some Atom) -> AtomProducer { + AtomProducer { context in + context.transaction { context in + let value = context.watch(atom) + let storage = context.watch(StorageAtom()) + let previous = storage.previous + storage.previous = value + return previous + } + } + } +} + +private extension PreviousModifier { + @MainActor + final class Storage { + var previous: Base? + } + + struct StorageAtom: ValueAtom, Hashable { + func value(context: Context) -> Storage { + Storage() + } + } +} diff --git a/Tests/AtomsTests/Modifier/PreviousModifierTests.swift b/Tests/AtomsTests/Modifier/PreviousModifierTests.swift new file mode 100644 index 00000000..e2e4bf4b --- /dev/null +++ b/Tests/AtomsTests/Modifier/PreviousModifierTests.swift @@ -0,0 +1,87 @@ +import XCTest + +@testable import Atoms + +final class PreviousModifierTests: XCTestCase { + @MainActor + func testPrevious() { + let atom = TestStateAtom(defaultValue: "initial") + let context = AtomTestContext() + + XCTAssertNil(context.watch(atom.previous)) + + // Update the atom value + context[atom] = "second" + + // Now previous should return the initial value + XCTAssertEqual(context.watch(atom.previous), "initial") + + // Update again + context[atom] = "third" + + // Previous should now return "second" + XCTAssertEqual(context.watch(atom.previous), "second") + + // Another update + context[atom] = "fourth" + + // Previous should now return "third" + XCTAssertEqual(context.watch(atom.previous), "third") + } + + @MainActor + func testPreviousWithMultipleWatchers() { + let atom = TestStateAtom(defaultValue: 100) + let context = AtomTestContext() + + // Watch both current and previous + XCTAssertEqual(context.watch(atom), 100) + XCTAssertNil(context.watch(atom.previous)) + + // Update the value + context[atom] = 200 + + // Check both watchers + XCTAssertEqual(context.watch(atom), 200) + XCTAssertEqual(context.watch(atom.previous), 100) + + // Update again + context[atom] = 300 + + XCTAssertEqual(context.watch(atom), 300) + XCTAssertEqual(context.watch(atom.previous), 200) + } + + @MainActor + func testPreviousUpdatesDownstream() { + let atom = TestStateAtom(defaultValue: 0) + let context = AtomTestContext() + var updatedCount = 0 + + context.onUpdate = { + updatedCount += 1 + } + + // Initial watch + XCTAssertEqual(updatedCount, 0) + XCTAssertNil(context.watch(atom.previous)) + + // First update + context[atom] = 1 + XCTAssertEqual(updatedCount, 1) + XCTAssertEqual(context.watch(atom.previous), 0) + + // Second update + context[atom] = 2 + XCTAssertEqual(updatedCount, 2) + XCTAssertEqual(context.watch(atom.previous), 1) + } + + @MainActor + func testKey() { + let modifier = PreviousModifier() + + XCTAssertEqual(modifier.key, modifier.key) + XCTAssertEqual(modifier.key.hashValue, modifier.key.hashValue) + } +} From 555d217230970b1194e5fba3d2c1001ffa4f106d Mon Sep 17 00:00:00 2001 From: ra1028 Date: Fri, 3 Oct 2025 17:09:51 +0900 Subject: [PATCH 2/3] Update Atoms.md --- Sources/Atoms/Atoms.docc/Atoms.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Atoms/Atoms.docc/Atoms.md b/Sources/Atoms/Atoms.docc/Atoms.md index e8643a34..0ffbd6d8 100644 --- a/Sources/Atoms/Atoms.docc/Atoms.md +++ b/Sources/Atoms/Atoms.docc/Atoms.md @@ -28,6 +28,7 @@ Building state by compositing atoms automatically optimizes rendering based on i ### Modifiers +- ``Atom/previous`` - ``Atom/changes`` - ``Atom/changes(of:)`` - ``Atom/animation(_:)`` @@ -84,6 +85,7 @@ Building state by compositing atoms automatically optimizes rendering based on i - ``AtomStore`` - ``AtomModifier`` - ``AsyncAtomModifier`` +- ``PreviousModifier`` - ``ChangesModifier`` - ``ChangesOfModifier`` - ``TaskPhaseModifier`` From 38eff775c59740796a4fe97fd55a887d4801b37a Mon Sep 17 00:00:00 2001 From: ra1028 Date: Fri, 3 Oct 2025 17:13:22 +0900 Subject: [PATCH 3/3] Update README --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index a4161973..c96df35f 100644 --- a/README.md +++ b/README.md @@ -683,6 +683,45 @@ struct ContactView: View { Modifiers can be applied to an atom to produce a different versions of the original atom to make it more coding friendly or to reduce view re-computation for performance optimization. +#### [previous](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/previous) + +| |Description| +|:--------------|:----------| +|Summary |Provides the previous value of the atom instead of the current value.| +|Output |`T?`| +|Compatible |All atom types.| +|Use Case |Track value changes, Compare previous and current values| + +
📖 Example + +```swift +struct CounterAtom: StateAtom, Hashable { + func defaultValue(context: Context) -> Int { + 0 + } +} + +struct CounterView: View { + @WatchState(CounterAtom()) + var currentValue + + @Watch(CounterAtom().previous) + var previousValue + + var body: some View { + VStack { + Text("Current: \(currentValue)") + Text("Previous: \(previousValue ?? 0)") + Button("Increment") { + currentValue += 1 + } + } + } +} +``` + +
+ #### [changes(of:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/changes(of:)) | |Description|