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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1413,7 +1413,7 @@ struct PersistentCounterAtom: StateAtom, Hashable {
UserDefaults.standard.integer(forKey: "persistence_key")
}

func updated(newValue: Int, oldValue: Int, reader: Reader) {
func updated(newValue: Int, oldValue: Int, context: UpdatedContext) {
if newValue != oldValue {
UserDefaults.standard.set(newValue, forKey: "persistence_key")
}
Expand All @@ -1423,7 +1423,7 @@ struct PersistentCounterAtom: StateAtom, Hashable {

</details>

All atom types can optionally implement [`updated(newValue:oldValue:reader:`](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/updated(newvalue:oldvalue:reader:)-fgb8) method to manage arbitrary side-effects of value updates, such as state persistence, state synchronization, logging, and etc.
All atom types can optionally implement [`updated(newValue:oldValue:context:`](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/updated(newvalue:oldvalue:context:)-7kvo0) method to manage arbitrary side-effects of value updates, such as state persistence, state synchronization, logging, and etc.
In the above example, the initial state of the atom is retrieved from UserDefaults, and when the user updates the state, the value is reflected into UserDefaults as a side effect.

---
Expand Down
14 changes: 8 additions & 6 deletions Sources/Atoms/Atom/Atom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ public protocol Atom {
/// with other atoms.
typealias Context = AtomTransactionContext<Coordinator>

/// A type of the structure that to read value other atoms.
typealias Reader = AtomReader
/// A type of the context structure that to read, set, and otherwise interacting
/// with other atoms.
typealias UpdatedContext = AtomUpdatedContext

/// A boolean value indicating whether the atom value should be preserved even if
/// no longer watched to.
Expand Down Expand Up @@ -51,14 +52,15 @@ public protocol Atom {
///
/// Use it to manage arbitrary side-effects of value updates, such as state persistence,
/// state synchronization, logging, and etc.
/// You can also access other atom values via `reader` passed as a parameter.
/// You can also access other atom values via `context` passed as a parameter.
///
/// - Parameters:
/// - newValue: A new value after update.
/// - oldValue: An old value before update.
/// - reader: A structure that to read value other atoms.
/// - context: A context structure that to read, set, and otherwise
/// interacting with other atoms.
@MainActor
func updated(newValue: Loader.Value, oldValue: Loader.Value, reader: Reader)
func updated(newValue: Loader.Value, oldValue: Loader.Value, context: UpdatedContext)

// --- Internal ---

Expand All @@ -76,7 +78,7 @@ public extension Atom {
()
}

func updated(newValue: Loader.Value, oldValue: Loader.Value, reader: Reader) {}
func updated(newValue: Loader.Value, oldValue: Loader.Value, context: UpdatedContext) {}
}

public extension Atom where Self == Key {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Atoms/Atoms.docc/Atoms.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ SwiftUI Atom Properties offers practical capabilities to manage the complexity o
- ``AtomWatchableContext``
- ``AtomTransactionContext``
- ``AtomViewContext``
- ``AtomUpdatedContext``
- ``AtomTestContext``
- ``AtomReader``

### Internal System

Expand Down
29 changes: 0 additions & 29 deletions Sources/Atoms/Context/AtomReader.swift

This file was deleted.

94 changes: 94 additions & 0 deletions Sources/Atoms/Context/AtomUpdatedContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/// A context structure that to read, set, and otherwise interacting with atoms.
@MainActor
public struct AtomUpdatedContext: AtomContext {
@usableFromInline
internal let _store: StoreContext

internal init(store: StoreContext) {
self._store = store
}

/// Accesses the value associated with the given atom without watching to it.
///
/// This method returns a value for the given atom. Even if you access to a value with this method,
/// it doesn't initiating watch the atom, so if none of other atoms or views is watching as well,
/// the value will not be cached.
///
/// ```swift
/// let context = ...
/// print(context.read(TextAtom())) // Prints the current value associated with `TextAtom`.
/// ```
///
/// - Parameter atom: An atom that associates the value.
///
/// - Returns: The value associated with the given atom.
@inlinable
public func read<Node: Atom>(_ atom: Node) -> Node.Loader.Value {
_store.read(atom)
}

/// Sets the new value for the given writable atom.
///
/// This method only accepts writable atoms such as types conforming to ``StateAtom``,
/// and assign a new value for the atom.
/// When you assign a new value, it notifies update immediately to downstream atoms or views.
///
/// - SeeAlso: ``AtomViewContext/subscript``
///
/// ```swift
/// let context = ...
/// context.set("New text", for: TextAtom())
/// print(context.read(TextAtom())) // Prints "New text"
/// ```
///
/// - Parameters
/// - value: A value to be set.
/// - atom: An atom that associates the value.
@inlinable
public func set<Node: StateAtom>(_ value: Node.Loader.Value, for atom: Node) {
_store.set(value, for: atom)
}

/// Refreshes and then return the value associated with the given refreshable atom.
///
/// This method only accepts refreshable atoms such as types conforming to:
/// ``TaskAtom``, ``ThrowingTaskAtom``, ``AsyncSequenceAtom``, ``PublisherAtom``.
/// It refreshes the value for the given atom and then return, so the caller can await until
/// the value completes the update.
/// Note that it can be used only in a context that supports concurrency.
///
/// ```swift
/// let context = ...
/// let image = await context.refresh(AsyncImageDataAtom()).value
/// print(image) // Prints the data obtained through network.
/// ```
///
/// - Parameter atom: An atom that associates the value.
///
/// - Returns: The value which completed refreshing associated with the given atom.
@discardableResult
@inlinable
public func refresh<Node: Atom>(_ atom: Node) async -> Node.Loader.Value where Node.Loader: RefreshableAtomLoader {
await _store.refresh(atom)
}

/// Resets the value associated with the given atom, and then notify.
///
/// This method resets a value for the given atom, and then notify update to the downstream
/// atoms and views. Thereafter, if any of other atoms or views is watching the atom, a newly
/// generated value will be produced.
///
/// ```swift
/// let context = ...
/// context[TextAtom()] = "New text"
/// print(context.read(TextAtom())) // Prints "New text"
/// context.reset(TextAtom())
/// print(context.read(TextAtom())) // Prints "Text"
/// ```
///
/// - Parameter atom: An atom that associates the value.
@inlinable
public func reset(_ atom: some Atom) {
_store.reset(atom)
}
}
4 changes: 2 additions & 2 deletions Sources/Atoms/Core/StoreContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,8 @@ private extension StoreContext {
}

func notifyUpdated() {
let reader = AtomReader(store: self)
atom.updated(newValue: value, oldValue: oldValue, reader: reader)
let context = AtomUpdatedContext(store: self)
atom.updated(newValue: value, oldValue: oldValue, context: context)
}

// Ensures the value is updated.
Expand Down
2 changes: 1 addition & 1 deletion Tests/AtomsTests/Atom/ObservableObjectAtomTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ final class ObservableObjectAtomTests: XCTestCase {
TestObject()
}

func updated(newValue: TestObject, oldValue: TestObject, reader: Reader) {
func updated(newValue: TestObject, oldValue: TestObject, context: UpdatedContext) {
onUpdated?(newValue, oldValue)
}
}
Expand Down
58 changes: 58 additions & 0 deletions Tests/AtomsTests/Context/AtomUpdatedContextTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import XCTest

@testable import Atoms

@MainActor
final class AtomUpdatedContextTests: XCTestCase {
func testRead() {
let atom = TestValueAtom(value: 100)
let store = Store()
let context = AtomUpdatedContext(store: StoreContext(store))

XCTAssertEqual(context.read(atom), 100)
}

func testSet() {
let atom = TestValueAtom(value: 0)
let dependency = TestStateAtom(defaultValue: 100)
let store = Store()
let transaction = Transaction(key: AtomKey(atom)) {}
let storeContext = StoreContext(store)
let context = AtomUpdatedContext(store: storeContext)

XCTAssertEqual(storeContext.watch(dependency, in: transaction), 100)

context.set(200, for: dependency)

XCTAssertEqual(storeContext.watch(dependency, in: transaction), 200)
}

func testRefresh() async {
let atom = TestTaskAtom(value: 100)
let store = Store()
let context = AtomUpdatedContext(store: StoreContext(store))

let value = await context.refresh(atom).value

XCTAssertEqual(value, 100)
}

func testReset() {
let atom = TestValueAtom(value: 0)
let dependency = TestStateAtom(defaultValue: 0)
let store = Store()
let transaction = Transaction(key: AtomKey(atom)) {}
let storeContext = StoreContext(store)
let context = AtomTransactionContext(store: StoreContext(store), transaction: transaction, coordinator: ())

XCTAssertEqual(storeContext.watch(dependency, in: transaction), 0)

context[dependency] = 100

XCTAssertEqual(storeContext.watch(dependency, in: transaction), 100)

context.reset(dependency)

XCTAssertEqual(context.read(dependency), 0)
}
}
12 changes: 6 additions & 6 deletions Tests/AtomsTests/Utilities/TestAtom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct TestValueAtom<T>: ValueAtom {
value
}

func updated(newValue: T, oldValue: T, reader: Reader) {
func updated(newValue: T, oldValue: T, context: UpdatedContext) {
onUpdated?(newValue, oldValue)
}
}
Expand All @@ -38,7 +38,7 @@ struct TestStateAtom<T>: StateAtom {
defaultValue
}

func updated(newValue: T, oldValue: T, reader: Reader) {
func updated(newValue: T, oldValue: T, context: UpdatedContext) {
onUpdated?(newValue, oldValue)
}
}
Expand Down Expand Up @@ -70,7 +70,7 @@ struct TestTaskAtom<T: Sendable>: TaskAtom {
func updated(
newValue: Task<T, Never>,
oldValue: Task<T, Never>,
reader: Reader
context: UpdatedContext
) {
onUpdated?(newValue, oldValue)
}
Expand Down Expand Up @@ -103,7 +103,7 @@ struct TestThrowingTaskAtom<Success: Sendable>: ThrowingTaskAtom {
func updated(
newValue: Task<Success, Error>,
oldValue: Task<Success, Error>,
reader: Reader
context: UpdatedContext
) {
onUpdated?(newValue, oldValue)
}
Expand All @@ -124,7 +124,7 @@ struct TestPublisherAtom<Publisher: Combine.Publisher>: PublisherAtom {
func updated(
newValue: AsyncPhase<Publisher.Output, Publisher.Failure>,
oldValue: AsyncPhase<Publisher.Output, Publisher.Failure>,
reader: Reader
context: UpdatedContext
) {
onUpdated?(newValue, oldValue)
}
Expand All @@ -145,7 +145,7 @@ struct TestAsyncSequenceAtom<Sequence: AsyncSequence>: AsyncSequenceAtom {
func updated(
newValue: AsyncPhase<Sequence.Element, Error>,
oldValue: AsyncPhase<Sequence.Element, Error>,
reader: Reader
context: UpdatedContext
) {
onUpdated?(newValue, oldValue)
}
Expand Down