Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Implement previous modifier
  • Loading branch information
ra1028 committed Oct 3, 2025
commit 7641387d52cf1b25c1d8dacb830b522008e2b0a2
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)
}
}