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
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|

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

```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
}
}
}
}
```

</details>

#### [changes(of:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/changes(of:))

| |Description|
Expand Down
2 changes: 2 additions & 0 deletions Sources/Atoms/Atoms.docc/Atoms.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(_:)``
Expand Down Expand Up @@ -84,6 +85,7 @@ Building state by compositing atoms automatically optimizes rendering based on i
- ``AtomStore``
- ``AtomModifier``
- ``AsyncAtomModifier``
- ``PreviousModifier``
- ``ChangesModifier``
- ``ChangesOfModifier``
- ``TaskPhaseModifier``
Expand Down
75 changes: 75 additions & 0 deletions Sources/Atoms/Modifier/PreviousModifier.swift
Original file line number Diff line number Diff line change
@@ -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<Self, PreviousModifier<Produced>> {
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<Base>: 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<Base>) -> AtomProducer<Produced> {
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()
}
}
}
87 changes: 87 additions & 0 deletions Tests/AtomsTests/Modifier/PreviousModifierTests.swift
Original file line number Diff line number Diff line change
@@ -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<Int>()

XCTAssertEqual(modifier.key, modifier.key)
XCTAssertEqual(modifier.key.hashValue, modifier.key.hashValue)
}
}