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
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ struct TodoListScreen: View {
TodoItem(todo: todo)
}
.onDelete { indexSet in
let todos = TodosAtom()
let indices = indexSet.compactMap { index in
context[todos].firstIndex(of: filteredTodos[index])
let filtered = filteredTodos
context.modify(TodosAtom()) { todos in
let indices = indexSet.compactMap { index in
todos.firstIndex(of: filtered[index])
}
todos.remove(atOffsets: IndexSet(indices))
}
context[todos].remove(atOffsets: IndexSet(indices))
}
}
}
Expand Down
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -902,13 +902,14 @@ Context is a structure for using and interacting with atom values from views or

|API|Use|
|:--|:--|
|[watch(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomwatchablecontext/watch(_:))|Obtains an atom value and starts watching its update.|
|[read(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/read(_:))|Obtains an atom value but does not watch its update.|
|[set(_:for:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/set(_:for:))|Sets a new value to the atom.|
|[[:_]](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/subscript(_:)) subscript|Read-write access for applying mutating methods.|
|[state(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomwatchablecontext/state(_:))|Gets a binding to the atom state.|
|[refresh(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/refresh(_:))|Reset an atom and await until asynchronous operation is complete.|
|[reset(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/reset(_:))|Reset an atom to the default value or a first output.|
|[watch](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomwatchablecontext/watch(_:))|Obtains an atom value and starts watching its update.|
|[read](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/read(_:))|Obtains an atom value but does not watch its update.|
|[set](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/set(_:for:))|Sets a new value to the atom.|
|[modify](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/modify(_:body:))|Modifies the cached atom value.|
|[subscript[]](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/subscript(_:))|Read-write access for applying mutating methods.|
|[state](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomwatchablecontext/state(_:))|Gets a binding to the atom state.|
|[refresh](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/refresh(_:))|Reset an atom and await until asynchronous operation is complete.|
|[reset](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/reset(_:))|Reset an atom to the default value or a first output.|

There are the following types context as different contextual environments.
The APIs described in each section below are their own specific functionality depending on the environment in which it is used, in addition to the above common APIs.
Expand Down
21 changes: 21 additions & 0 deletions Sources/Atoms/Context/AtomContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ public protocol AtomContext {
/// - atom: An atom that associates the value.
func set<Node: StateAtom>(_ value: Node.Loader.Value, for atom: Node)

/// Modifies the cached value of 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 modify value, it notifies update to downstream atoms or views after all
/// the modification completed.
///
/// ```swift
/// let context = ...
/// print(context.watch(TextAtom())) // Prints "Text"
/// context.modify(TextAtom()) { text in
/// text.append(" modified")
/// }
/// print(context.read(TextAtom())) // Prints "Text modified"
/// ```
///
/// - Parameters
/// - atom: An atom that associates the value.
/// - body: A value modification body.
func modify<Node: StateAtom>(_ atom: Node, body: (inout Node.Loader.Value) -> Void)

/// Refreshes and then return the value associated with the given refreshable atom.
///
/// This method only accepts refreshable atoms such as types conforming to:
Expand Down
23 changes: 23 additions & 0 deletions Sources/Atoms/Context/AtomTestContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,29 @@ public struct AtomTestContext: AtomWatchableContext {
store.set(value, for: atom)
}

/// Modifies the cached value of 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 modify value, it notifies update to downstream atoms or views after all
/// the modification completed.
///
/// ```swift
/// let context = ...
/// print(context.watch(TextAtom())) // Prints "Text"
/// context.modify(TextAtom()) { text in
/// text.append(" modified")
/// }
/// print(context.read(TextAtom())) // Prints "Text modified"
/// ```
///
/// - Parameters
/// - atom: An atom that associates the value.
/// - body: A value modification body.
public func modify<Node: StateAtom>(_ atom: Node, body: (inout Node.Loader.Value) -> Void) {
store.modify(atom, body: body)
}

/// Refreshes and then return the value associated with the given refreshable atom.
///
/// This method only accepts refreshable atoms such as types conforming to:
Expand Down
24 changes: 24 additions & 0 deletions Sources/Atoms/Context/AtomTransactionContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,30 @@ public struct AtomTransactionContext<Coordinator>: AtomWatchableContext {
_store.set(value, for: atom)
}

/// Modifies the cached value of 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 modify value, it notifies update to downstream atoms or views after all
/// the modification completed.
///
/// ```swift
/// let context = ...
/// print(context.watch(TextAtom())) // Prints "Text"
/// context.modify(TextAtom()) { text in
/// text.append(" modified")
/// }
/// print(context.read(TextAtom())) // Prints "Text modified"
/// ```
///
/// - Parameters
/// - atom: An atom that associates the value.
/// - body: A value modification body.
@inlinable
public func modify<Node: StateAtom>(_ atom: Node, body: (inout Node.Loader.Value) -> Void) {
_store.modify(atom, body: body)
}

/// Refreshes and then return the value associated with the given refreshable atom.
///
/// This method only accepts refreshable atoms such as types conforming to:
Expand Down
24 changes: 24 additions & 0 deletions Sources/Atoms/Context/AtomUpdatedContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,30 @@ public struct AtomUpdatedContext<Coordinator>: AtomContext {
_store.set(value, for: atom)
}

/// Modifies the cached value of 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 modify value, it notifies update to downstream atoms or views after all
/// the modification completed.
///
/// ```swift
/// let context = ...
/// print(context.watch(TextAtom())) // Prints "Text"
/// context.modify(TextAtom()) { text in
/// text.append(" modified")
/// }
/// print(context.read(TextAtom())) // Prints "Text modified"
/// ```
///
/// - Parameters
/// - atom: An atom that associates the value.
/// - body: A value modification body.
@inlinable
public func modify<Node: StateAtom>(_ atom: Node, body: (inout Node.Loader.Value) -> Void) {
_store.modify(atom, body: body)
}

/// Refreshes and then return the value associated with the given refreshable atom.
///
/// This method only accepts refreshable atoms such as types conforming to:
Expand Down
24 changes: 24 additions & 0 deletions Sources/Atoms/Context/AtomViewContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,30 @@ public struct AtomViewContext: AtomWatchableContext {
_store.set(value, for: atom)
}

/// Modifies the cached value of 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 modify value, it notifies update to downstream atoms or views after all
/// the modification completed.
///
/// ```swift
/// let context = ...
/// print(context.watch(TextAtom())) // Prints "Text"
/// context.modify(TextAtom()) { text in
/// text.append(" modified")
/// }
/// print(context.read(TextAtom())) // Prints "Text modified"
/// ```
///
/// - Parameters
/// - atom: An atom that associates the value.
/// - body: A value modification body.
@inlinable
public func modify<Node: StateAtom>(_ atom: Node, body: (inout Node.Loader.Value) -> Void) {
_store.modify(atom, body: body)
}

/// Refreshes and then return the value associated with the given refreshable atom.
///
/// This method only accepts refreshable atoms such as types conforming to:
Expand Down
23 changes: 23 additions & 0 deletions Sources/Atoms/Core/StoreContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ internal struct StoreContext {
set(value, for: atom, scope: current)
}

@usableFromInline
func modify<Node: StateAtom>(_ atom: Node, body: (inout Node.Loader.Value) -> Void) {
modify(atom, scope: current, body: body)
}

@usableFromInline
func watch<Node: Atom>(_ atom: Node, in transaction: Transaction) -> Node.Loader.Value {
watch(atom, in: transaction, scope: current)
Expand Down Expand Up @@ -127,6 +132,24 @@ private extension StoreContext {
}
}

func modify<Node: StateAtom>(_ atom: Node, scope: Scope, body: (inout Node.Loader.Value) -> Void) {
let key = AtomKey(atom)

if let cache = peekCache(of: atom, for: key, scope: scope) {
var value = cache.value
body(&value)
update(atom: atom, for: key, value: value, cache: cache, scope: scope)
checkRelease(for: key, scope: scope)
}
else if scope.overrides.hasValue(for: key) {
// Do nothing if the atom is overridden.
return
}
else if let parent = scope.parent {
return modify(atom, scope: parent, body: body)
}
}

func watch<Node: Atom>(_ atom: Node, in transaction: Transaction, scope: Scope) -> Node.Loader.Value {
guard !transaction.isTerminated else {
return read(atom)
Expand Down
60 changes: 55 additions & 5 deletions Tests/AtomsTests/Core/StoreContextTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,42 @@ final class StoreContextTests: XCTestCase {
XCTAssertEqual((store.state.caches[key] as? AtomCache<TestStateAtom<Int>>)?.value, 3)
}

func testModify() {
let store = AtomStore()
let context = StoreContext(store)
let subscriptionKey = SubscriptionKey(SubscriptionContainer())
var updateCount = 0
let atom = TestStateAtom(defaultValue: 0)
let key = AtomKey(atom)

context.set(1, for: atom)
context.modify(atom) { $0 = 1 }

XCTAssertEqual(updateCount, 0)
XCTAssertNil(store.state.states[key])
XCTAssertNil(store.state.caches[key])

store.state.caches[key] = AtomCache(atom: atom, value: 0)
store.state.states[key] = AtomState(coordinator: atom.makeCoordinator())
store.state.subscriptions[key, default: [:]][subscriptionKey] = Subscription(
notifyUpdate: { updateCount += 1 },
unsubscribe: {}
)

context.modify(atom) { $0 = 2 }

XCTAssertEqual(updateCount, 1)
XCTAssertNil(store.state.states[key]?.transaction)
XCTAssertEqual((store.state.caches[key] as? AtomCache<TestStateAtom<Int>>)?.value, 2)

context.modify(atom) { $0 = 3 }

XCTAssertEqual(updateCount, 2)
XCTAssertNotNil(store.state.states[key])
XCTAssertNil(store.state.states[key]?.transaction)
XCTAssertEqual((store.state.caches[key] as? AtomCache<TestStateAtom<Int>>)?.value, 3)
}

func testWatch() {
let store = AtomStore()
let context = StoreContext(store)
Expand Down Expand Up @@ -249,7 +285,7 @@ final class StoreContextTests: XCTestCase {
XCTAssertTrue(store.state.caches.isEmpty)
}

func testScopedObservers() {
func testScoped() {
let store = AtomStore()
let container = SubscriptionContainer()
let atom = TestValueAtom(value: 0)
Expand Down Expand Up @@ -340,15 +376,17 @@ final class StoreContextTests: XCTestCase {

XCTAssertEqual(context.watch(dependency1Atom, container: container.wrapper) {}, 0)

// Shouldn't set value if the atom is overridden in the scope.
scoped1Context.set(1, for: dependency1Atom)
XCTAssertEqual(context.read(dependency1Atom), 0)

// Shouldn't set value if the atom is overridden in the scope.
// Shouldn't modify value if the atom is overridden in the scope.
scoped1Context.modify(dependency1Atom) { $0 = 1 }
XCTAssertEqual(context.read(dependency1Atom), 0)

// Shouldn't reset value if the atom is overridden in the scope.
context.set(1, for: dependency1Atom)
scoped1Context.reset(dependency1Atom)

// Shouldn't reset value if the atom is overridden in the scope.
XCTAssertEqual(context.read(dependency1Atom), 1)

container.wrapper.subscriptions[AtomKey(dependency1Atom)]?.unsubscribe()
Expand All @@ -373,9 +411,21 @@ final class StoreContextTests: XCTestCase {
XCTAssertEqual(scoped2Context.read(dependency2Atom), 100)
XCTAssertEqual(context.read(atom), 120)

// Should modify the value and then propagate it to the dependent atoms..
scoped2Context.modify(dependency1Atom) { $0 = 40 }

// Shouldn't modify the value here because the atom is cached in the scoped store.
context.modify(dependency1Atom) { $0 = 50 }

// Should return overridden values.
XCTAssertEqual(scoped2Context.read(atom), 140)
XCTAssertEqual(scoped2Context.read(dependency1Atom), 40)
XCTAssertEqual(scoped2Context.read(dependency2Atom), 100)
XCTAssertEqual(context.read(atom), 140)

do {
let phase = await scoped2Context.refresh(publisherAtom)
XCTAssertEqual(phase, .success(120))
XCTAssertEqual(phase, .success(140))
}

// Should reset the value and then propagate it to the dependent atoms..
Expand Down