Skip to content

Commit e8ce984

Browse files
authored
Merge pull request #10 from ra1028/feat/observable-object-did-update
feat: Ensure that ObservableObjectAtom notifies updates after a new @published value is set
2 parents a9278af + ae98103 commit e8ce984

File tree

11 files changed

+206
-39
lines changed

11 files changed

+206
-39
lines changed

Sources/Atoms/Context/AtomContext.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,26 @@ public protocol AtomWatchableContext: AtomContext {
130130
/// - Returns: The value associated with the given atom.
131131
@discardableResult
132132
func watch<Node: Atom>(_ atom: Node) -> Node.Hook.Value
133+
134+
/// Accesses the observable object associated with the given atom for reading and initialing watch to
135+
/// receive its updates.
136+
///
137+
/// This method returns an observable object for the given atom and initiate watching the atom so that
138+
/// the current context to get updated when the atom notifies updates.
139+
/// The observable object associated with the atom is cached until it is no longer watched to or until
140+
/// it is updated.
141+
///
142+
/// ```swift
143+
/// let context = ...
144+
/// let store = context.watch(AccountStoreAtom())
145+
/// print(store.currentUser) // Prints the user value after update.
146+
/// ```
147+
///
148+
/// - Parameter atom: An atom that associates the observable object.
149+
///
150+
/// - Returns: The observable object associated with the given atom.
151+
@discardableResult
152+
func watch<Node: Atom>(_ atom: Node) -> Node.Hook.Value where Node.Hook: AtomObservableObjectHook
133153
}
134154

135155
public extension AtomWatchableContext {

Sources/Atoms/Context/AtomRelationContext.swift

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,30 @@ public struct AtomRelationContext: AtomWatchableContext {
117117
@inlinable
118118
@discardableResult
119119
public func watch<Node: Atom>(_ atom: Node) -> Node.Hook.Value {
120-
_box.watch(atom)
120+
_box.watch(atom, shouldNotifyAfterUpdates: false)
121+
}
122+
123+
/// Accesses the observable object associated with the given atom for reading and initialing watch to
124+
/// receive its updates.
125+
///
126+
/// This method returns an observable object for the given atom and initiate watching the atom so that
127+
/// the current context to get updated when the atom notifies updates.
128+
/// The observable object associated with the atom is cached until it is no longer watched to or until
129+
/// it is updated.
130+
///
131+
/// ```swift
132+
/// let context = ...
133+
/// let store = context.watch(AccountStoreAtom())
134+
/// print(store.currentUser) // Prints the user value after update.
135+
/// ```
136+
///
137+
/// - Parameter atom: An atom that associates the observable object.
138+
///
139+
/// - Returns: The observable object associated with the given atom.
140+
@inlinable
141+
@discardableResult
142+
public func watch<Node: Atom>(_ atom: Node) -> Node.Hook.Value where Node.Hook: AtomObservableObjectHook {
143+
_box.watch(atom, shouldNotifyAfterUpdates: true)
121144
}
122145

123146
/// Add the termination action that will be performed when the atom will no longer be watched to
@@ -175,7 +198,7 @@ public struct AtomRelationContext: AtomWatchableContext {
175198
internal protocol _AnyAtomRelationContextBox {
176199
var store: AtomStore { get }
177200

178-
func watch<Node: Atom>(_ atom: Node) -> Node.Hook.Value
201+
func watch<Node: Atom>(_ atom: Node, shouldNotifyAfterUpdates: Bool) -> Node.Hook.Value
179202
func addTermination(_ termination: @MainActor @escaping () -> Void)
180203
func keepUntilTermination<Object: AnyObject>(_ object: Object)
181204
}
@@ -200,8 +223,12 @@ internal struct _AtomRelationContextBox<Caller: Atom>: _AnyAtomRelationContextBo
200223
let store: AtomStore
201224

202225
@usableFromInline
203-
func watch<Node: Atom>(_ atom: Node) -> Node.Hook.Value {
204-
store.watch(atom, belongTo: caller)
226+
func watch<Node: Atom>(_ atom: Node, shouldNotifyAfterUpdates: Bool) -> Node.Hook.Value {
227+
store.watch(
228+
atom,
229+
belongTo: caller,
230+
shouldNotifyAfterUpdates: shouldNotifyAfterUpdates
231+
)
205232
}
206233

207234
@usableFromInline

Sources/Atoms/Context/AtomTestContext.swift

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,29 @@ public struct AtomTestContext: AtomWatchableContext {
120120
/// - Returns: The value associated with the given atom.
121121
@discardableResult
122122
public func watch<Node: Atom>(_ atom: Node) -> Node.Hook.Value {
123-
container.store.watch(atom, relationship: container.relationship) {
124-
container.onUpdate?()
125-
}
123+
container.watch(atom, shouldNotifyAfterUpdates: false)
124+
}
125+
126+
/// Accesses the observable object associated with the given atom for reading and initialing watch to
127+
/// receive its updates.
128+
///
129+
/// This method returns an observable object for the given atom and initiate watching the atom so that
130+
/// the current context to get updated when the atom notifies updates.
131+
/// The observable object associated with the atom is cached until it is no longer watched to or until
132+
/// it is updated.
133+
///
134+
/// ```swift
135+
/// let context = ...
136+
/// let store = context.watch(AccountStoreAtom())
137+
/// print(store.currentUser) // Prints the user value after update.
138+
/// ```
139+
///
140+
/// - Parameter atom: An atom that associates the observable object.
141+
///
142+
/// - Returns: The observable object associated with the given atom.
143+
@discardableResult
144+
public func watch<Node: Atom>(_ atom: Node) -> Node.Hook.Value where Node.Hook: AtomObservableObjectHook {
145+
container.watch(atom, shouldNotifyAfterUpdates: true)
126146
}
127147

128148
/// Unwatches the given atom and do not receive any more updates of it.
@@ -199,5 +219,15 @@ private extension AtomTestContext {
199219
var relationship: Relationship {
200220
Relationship(container: relationshipContainer)
201221
}
222+
223+
func watch<Node: Atom>(_ atom: Node, shouldNotifyAfterUpdates: Bool) -> Node.Hook.Value {
224+
store.watch(
225+
atom,
226+
relationship: relationship,
227+
shouldNotifyAfterUpdates: shouldNotifyAfterUpdates
228+
) { [weak self] in
229+
self?.onUpdate?()
230+
}
231+
}
202232
}
203233
}

Sources/Atoms/Context/AtomViewContext.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ public struct AtomViewContext: AtomWatchableContext {
127127
@discardableResult
128128
@inlinable
129129
public func watch<Node: Atom>(_ atom: Node) -> Node.Hook.Value {
130-
_store.watch(atom, relationship: _relationship, notifyUpdate: _notifyUpdate)
130+
_store.watch(
131+
atom,
132+
relationship: _relationship,
133+
shouldNotifyAfterUpdates: false,
134+
notifyUpdate: _notifyUpdate
135+
)
131136
}
132137
}

Sources/Atoms/Core/Hook/AtomHook.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import Combine
2+
13
/// Internal use, a hook type that determines behavioral details of atoms.
24
@MainActor
35
public protocol AtomHook {
@@ -20,6 +22,10 @@ public protocol AtomHook {
2022
func updateOverride(context: Context, with value: Value)
2123
}
2224

25+
/// Internal use, a hook type that determines behavioral details of atoms which provide `ObservableObject`.
26+
@MainActor
27+
public protocol AtomObservableObjectHook: AtomHook where Value: ObservableObject {}
28+
2329
/// Internal use, a hook type that determines behavioral details of read-write atoms.
2430
@MainActor
2531
public protocol AtomStateHook: AtomHook {

Sources/Atoms/Core/Hook/ObservableObjectHook.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Combine
22

33
/// Internal use, a hook type that determines behavioral details of corresponding atoms.
44
@MainActor
5-
public struct ObservableObjectHook<ObjectType: ObservableObject>: AtomHook {
5+
public struct ObservableObjectHook<ObjectType: ObservableObject>: AtomObservableObjectHook {
66
/// A reference type object to manage internal state.
77
public final class Coordinator {
88
internal var object: ObjectType?

Sources/Atoms/Core/Internal/AtomStore.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,16 @@ internal protocol AtomStore {
2525
func watch<Node: Atom>(
2626
_ atom: Node,
2727
relationship: Relationship,
28+
shouldNotifyAfterUpdates: Bool,
2829
notifyUpdate: @MainActor @escaping () -> Void
2930
) -> Node.Hook.Value
3031

3132
@MainActor
32-
func watch<Node: Atom, Caller: Atom>(_ atom: Node, belongTo caller: Caller) -> Node.Hook.Value
33+
func watch<Node: Atom, Caller: Atom>(
34+
_ atom: Node,
35+
belongTo caller: Caller,
36+
shouldNotifyAfterUpdates: Bool
37+
) -> Node.Hook.Value
3338

3439
@MainActor
3540
func notifyUpdate<Node: Atom>(_ atom: Node)

Sources/Atoms/Core/Internal/DefaultStore.swift

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,29 @@ internal struct DefaultStore: AtomStore {
3636
func watch<Node: Atom>(
3737
_ atom: Node,
3838
relationship: Relationship,
39+
shouldNotifyAfterUpdates: Bool,
3940
notifyUpdate: @escaping @MainActor () -> Void
4041
) -> Node.Hook.Value {
4142
assertionFailureStoreNotProvided()
42-
return fallbackStore.watch(atom, relationship: relationship, notifyUpdate: notifyUpdate)
43+
return fallbackStore.watch(
44+
atom,
45+
relationship: relationship,
46+
shouldNotifyAfterUpdates: shouldNotifyAfterUpdates,
47+
notifyUpdate: notifyUpdate
48+
)
4349
}
4450

45-
func watch<Node: Atom, Caller: Atom>(_ atom: Node, belongTo caller: Caller) -> Node.Hook.Value {
51+
func watch<Node: Atom, Caller: Atom>(
52+
_ atom: Node,
53+
belongTo caller: Caller,
54+
shouldNotifyAfterUpdates: Bool
55+
) -> Node.Hook.Value {
4656
assertionFailureStoreNotProvided()
47-
return fallbackStore.watch(atom, belongTo: caller)
57+
return fallbackStore.watch(
58+
atom,
59+
belongTo: caller,
60+
shouldNotifyAfterUpdates: shouldNotifyAfterUpdates
61+
)
4862
}
4963

5064
func notifyUpdate<Node: Atom>(_ atom: Node) {

Sources/Atoms/Core/Internal/Store.swift

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import Foundation
2+
13
@MainActor
24
internal struct Store: AtomStore {
35
private(set) weak var container: StoreContainer?
@@ -85,15 +87,29 @@ internal struct Store: AtomStore {
8587
func watch<Node: Atom>(
8688
_ atom: Node,
8789
relationship: Relationship,
90+
shouldNotifyAfterUpdates: Bool = false,
8891
notifyUpdate: @MainActor @escaping () -> Void
8992
) -> Node.Hook.Value {
9093
// Assign the observation to the given relationship.
91-
relationship[atom] = host(of: atom).observe(notifyUpdate)
94+
relationship[atom] = host(of: atom).observe {
95+
if shouldNotifyAfterUpdates {
96+
RunLoop.current.perform {
97+
notifyUpdate()
98+
}
99+
}
100+
else {
101+
notifyUpdate()
102+
}
103+
}
92104
return read(atom)
93105
}
94106

95-
func watch<Node: Atom, Caller: Atom>(_ atom: Node, belongTo caller: Caller) -> Node.Hook.Value {
96-
watch(atom, relationship: host(of: caller).relationship) {
107+
func watch<Node: Atom, Caller: Atom>(
108+
_ atom: Node,
109+
belongTo caller: Caller,
110+
shouldNotifyAfterUpdates: Bool = false
111+
) -> Node.Hook.Value {
112+
watch(atom, relationship: host(of: caller).relationship, shouldNotifyAfterUpdates: shouldNotifyAfterUpdates) {
97113
let oldValue = read(caller)
98114

99115
// Terminate the value & the ongoing task, but keep assignment until finishing notify update.

Tests/AtomsTests/Atom/ObservableObjectAtomTests.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,17 @@ final class ObservableObjectAtomTests: XCTestCase {
2525
let atom = TestAtom(value: 100)
2626
let context = AtomTestContext()
2727
let object = context.watch(atom)
28-
var isUpdated = false
28+
let expectation = expectation(description: "test")
29+
var updatedValue: Int?
2930

30-
XCTAssertEqual(object.value, 100)
31-
XCTAssertFalse(isUpdated)
31+
context.onUpdate = {
32+
updatedValue = object.value
33+
expectation.fulfill()
34+
}
3235

33-
context.onUpdate = { isUpdated = true }
3436
object.value = 200
3537

36-
XCTAssertEqual(object.value, 200)
37-
XCTAssertTrue(isUpdated)
38+
wait(for: [expectation], timeout: 1)
39+
XCTAssertEqual(updatedValue, 200)
3840
}
3941
}

0 commit comments

Comments
 (0)