Skip to content

Commit 2e93b28

Browse files
authored
Animation Modifier (#117)
* Implement animation modifier * Do not notify update if the object is already terminated * Weak capture
1 parent 502d34a commit 2e93b28

23 files changed

+270
-174
lines changed

Sources/Atoms/Atoms.docc/Atoms.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Building state by compositing atoms automatically optimizes rendering based on i
3030
- ``Atom/changes``
3131
- ``Atom/changes(of:)``
3232
- ``Atom/phase``
33+
- ``Atom/animation(_:)``
3334

3435
### Attributes
3536

@@ -75,6 +76,7 @@ Building state by compositing atoms automatically optimizes rendering based on i
7576
- ``ChangesModifier``
7677
- ``ChangesOfModifier``
7778
- ``TaskPhaseModifier``
79+
- ``AnimationModifier``
7880
- ``AtomLoader``
7981
- ``RefreshableAtomLoader``
8082
- ``AsyncAtomLoader``

Sources/Atoms/Context/AtomTestContext.swift

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,11 @@ public struct AtomTestContext: AtomWatchableContext {
321321
@inlinable
322322
@discardableResult
323323
public func watch<Node: Atom>(_ atom: Node) -> Node.Loader.Value {
324-
_store.watch(atom, subscriber: _subscriber, requiresObjectUpdate: true) { [weak _state] in
325-
_state?.notifyUpdate()
326-
}
324+
_store.watch(
325+
atom,
326+
subscriber: _subscriber,
327+
subscription: _subscription
328+
)
327329
}
328330

329331
/// Returns the already cached value associated with a given atom without side effects.
@@ -430,7 +432,7 @@ internal extension AtomTestContext {
430432
}
431433

432434
@usableFromInline
433-
func notifyUpdate() {
435+
func update() {
434436
onUpdate?()
435437
notifier.send()
436438
}
@@ -451,6 +453,13 @@ internal extension AtomTestContext {
451453

452454
@usableFromInline
453455
var _subscriber: Subscriber {
454-
Subscriber(_state.subscriberState, location: location)
456+
Subscriber(_state.subscriberState)
457+
}
458+
459+
@usableFromInline
460+
var _subscription: Subscription {
461+
Subscription(location: location) { [weak _state] in
462+
_state?.update()
463+
}
455464
}
456465
}

Sources/Atoms/Context/AtomViewContext.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ public struct AtomViewContext: AtomWatchableContext {
99
@usableFromInline
1010
internal let _subscriber: Subscriber
1111
@usableFromInline
12-
internal let _notifyUpdate: () -> Void
12+
internal let _subscription: Subscription
1313

1414
internal init(
1515
store: StoreContext,
1616
subscriber: Subscriber,
17-
notifyUpdate: @escaping () -> Void
17+
subscription: Subscription
1818
) {
1919
_store = store
2020
_subscriber = subscriber
21-
_notifyUpdate = notifyUpdate
21+
_subscription = subscription
2222
}
2323

2424
/// Accesses the value associated with the given atom without watching it.
@@ -198,8 +198,7 @@ public struct AtomViewContext: AtomWatchableContext {
198198
_store.watch(
199199
atom,
200200
subscriber: _subscriber,
201-
requiresObjectUpdate: false,
202-
notifyUpdate: _notifyUpdate
201+
subscription: _subscription
203202
)
204203
}
205204

Sources/Atoms/Core/Loader/AtomLoader.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,19 @@ public protocol AtomLoader {
1919
/// Returns a boolean value indicating whether it should notify updates downstream
2020
/// by checking the equivalence of the given old value and new value.
2121
func shouldUpdate(newValue: Value, oldValue: Value) -> Bool
22+
23+
/// Performs atom update.
24+
func performUpdate(_ body: () -> Void)
2225
}
2326

2427
public extension AtomLoader {
2528
func shouldUpdate(newValue: Value, oldValue: Value) -> Bool {
2629
true
2730
}
31+
32+
func performUpdate(_ body: () -> Void) {
33+
body()
34+
}
2835
}
2936

3037
/// A loader protocol that represents an actual implementation of the corresponding atom

Sources/Atoms/Core/Loader/AtomLoaderContext.swift

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,35 @@
11
/// The context structure to interact with an atom store.
22
@MainActor
33
public struct AtomLoaderContext<Value, Coordinator> {
4-
internal let store: StoreContext
5-
internal let transaction: Transaction
6-
internal let coordinator: Coordinator
7-
internal let update: @MainActor (Value, UpdateOrder) -> Void
4+
private let store: StoreContext
5+
private let transaction: Transaction
6+
private let coordinator: Coordinator
7+
private let update: @MainActor (Value) -> Void
88

99
internal init(
1010
store: StoreContext,
1111
transaction: Transaction,
1212
coordinator: Coordinator,
13-
update: @escaping @MainActor (Value, UpdateOrder) -> Void
13+
update: @escaping @MainActor (Value) -> Void
1414
) {
1515
self.store = store
1616
self.transaction = transaction
1717
self.coordinator = coordinator
1818
self.update = update
1919
}
2020

21+
internal var isTerminated: Bool {
22+
transaction.isTerminated
23+
}
24+
2125
internal var modifierContext: AtomModifierContext<Value> {
2226
AtomModifierContext(transaction: transaction) { value in
2327
update(with: value)
2428
}
2529
}
2630

27-
internal func update(with value: Value, order: UpdateOrder = .newValue) {
28-
update(value, order)
31+
internal func update(with value: Value) {
32+
update(value)
2933
}
3034

3135
internal func addTermination(_ termination: @MainActor @escaping () -> Void) {

Sources/Atoms/Core/Loader/ModifiedAtomLoader.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ public struct ModifiedAtomLoader<Node: Atom, Modifier: AtomModifier>: AtomLoader
3030
public func shouldUpdate(newValue: Value, oldValue: Value) -> Bool {
3131
modifier.shouldUpdate(newValue: newValue, oldValue: oldValue)
3232
}
33+
34+
/// Performs atom update.
35+
public func performUpdate(_ body: () -> Void) {
36+
modifier.performUpdate(body)
37+
}
3338
}
3439

3540
extension ModifiedAtomLoader: RefreshableAtomLoader where Node.Loader: RefreshableAtomLoader, Modifier: RefreshableAtomModifier {

Sources/Atoms/Core/Loader/ObservableObjectAtomLoader.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,18 @@ public struct ObservableObjectAtomLoader<Node: ObservableObjectAtom>: AtomLoader
2323

2424
/// Manage given overridden value updates and cancellations.
2525
public func manageOverridden(value: Value, context: Context) -> Value {
26-
let cancellable = value.objectWillChange.sink { [weak value] _ in
27-
guard let value else {
28-
return
26+
let cancellable = value
27+
.objectWillChange
28+
.sink { [weak value] _ in
29+
// Wait until the object's property is set, because `objectWillChange`
30+
// emits an event before the property is updated.
31+
RunLoop.main.perform(inModes: [.common]) {
32+
if !context.isTerminated, let value {
33+
context.update(with: value)
34+
}
35+
}
2936
}
3037

31-
context.update(with: value, order: .objectWillChange)
32-
}
33-
3438
context.addTermination(cancellable.cancel)
3539

3640
return value

Sources/Atoms/Core/StoreContext.swift

Lines changed: 24 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import Foundation
2-
31
@usableFromInline
42
@MainActor
53
internal struct StoreContext {
@@ -90,7 +88,7 @@ internal struct StoreContext {
9088
let key = AtomKey(atom, scopeKey: scopeKey)
9189

9290
if let cache = lookupCache(of: atom, for: key) {
93-
update(atom: atom, for: key, value: value, cache: cache, order: .newValue)
91+
update(atom: atom, for: key, newValue: value, cache: cache)
9492
}
9593
}
9694

@@ -101,9 +99,8 @@ internal struct StoreContext {
10199
let key = AtomKey(atom, scopeKey: scopeKey)
102100

103101
if let cache = lookupCache(of: atom, for: key) {
104-
var value = cache.value
105-
body(&value)
106-
update(atom: atom, for: key, value: value, cache: cache, order: .newValue)
102+
let newValue = mutating(cache.value, body)
103+
update(atom: atom, for: key, newValue: newValue, cache: cache)
107104
}
108105
}
109106

@@ -129,18 +126,12 @@ internal struct StoreContext {
129126
func watch<Node: Atom>(
130127
_ atom: Node,
131128
subscriber: Subscriber,
132-
requiresObjectUpdate: Bool,
133-
notifyUpdate: @escaping () -> Void
129+
subscription: Subscription
134130
) -> Node.Loader.Value {
135131
let override = lookupOverride(of: atom)
136132
let scopeKey = lookupScopeKey(of: atom, isScopedOverriden: override?.isScoped ?? false)
137133
let key = AtomKey(atom, scopeKey: scopeKey)
138134
let cache = getCache(of: atom, for: key, override: override)
139-
let subscription = Subscription(
140-
location: subscriber.location,
141-
requiresObjectUpdate: requiresObjectUpdate,
142-
notifyUpdate: notifyUpdate
143-
)
144135
let isNewSubscription = subscriber.subscribingKeys.insert(key).inserted
145136

146137
store.state.subscriptions[key, default: [:]].updateValue(subscription, forKey: subscriber.key)
@@ -177,8 +168,8 @@ internal struct StoreContext {
177168
}
178169

179170
// Notify update unless it's cancelled or terminated by other operations.
180-
if !Task.isCancelled && !context.transaction.isTerminated {
181-
update(atom: atom, for: key, value: value, cache: cache, order: .newValue)
171+
if !Task.isCancelled && !context.isTerminated {
172+
update(atom: atom, for: key, newValue: value, cache: cache)
182173
}
183174

184175
return value
@@ -199,7 +190,7 @@ internal struct StoreContext {
199190

200191
// Notify update unless it's cancelled or terminated by other operations.
201192
if !Task.isCancelled && !transaction.isTerminated {
202-
update(atom: atom, for: key, value: value, cache: cache, order: .newValue)
193+
update(atom: atom, for: key, newValue: value, cache: cache)
203194
}
204195

205196
return value
@@ -214,7 +205,7 @@ internal struct StoreContext {
214205

215206
if let cache = lookupCache(of: atom, for: key) {
216207
let newCache = makeCache(of: atom, for: key, override: override)
217-
update(atom: atom, for: key, value: newCache.value, cache: cache, order: .newValue)
208+
update(atom: atom, for: key, newValue: newCache.value, cache: cache)
218209
}
219210
}
220211

@@ -289,7 +280,7 @@ internal struct StoreContext {
289280
// Notify updates only for the subscriptions of restored atoms.
290281
if let subscriptions = store.state.subscriptions[key] {
291282
for subscription in ContiguousArray(subscriptions.values) {
292-
subscription.notifyUpdate()
283+
subscription.update()
293284
}
294285
}
295286
}
@@ -336,54 +327,38 @@ private extension StoreContext {
336327
store: self,
337328
transaction: transaction,
338329
coordinator: state.coordinator
339-
) { value, order in
340-
guard let cache = lookupCache(of: atom, for: key) else {
341-
return
330+
) { newValue in
331+
if let cache = lookupCache(of: atom, for: key) {
332+
update(atom: atom, for: key, newValue: newValue, cache: cache)
342333
}
343-
344-
update(
345-
atom: atom,
346-
for: key,
347-
value: value,
348-
cache: cache,
349-
order: order
350-
)
351334
}
352335
}
353336

354337
func update<Node: Atom>(
355338
atom: Node,
356339
for key: AtomKey,
357-
value: Node.Loader.Value,
358-
cache: AtomCache<Node>,
359-
order: UpdateOrder
340+
newValue: Node.Loader.Value,
341+
cache: AtomCache<Node>
360342
) {
361343
let oldValue = cache.value
362344

363-
if case .newValue = order {
364-
var cache = cache
365-
cache.value = value
366-
store.state.caches[key] = cache
345+
store.state.caches[key] = mutating(cache) {
346+
$0.value = newValue
367347
}
368348

369-
// Do not notify update if the new value and the old value are equivalent.
370-
if !atom._loader.shouldUpdate(newValue: value, oldValue: oldValue) {
349+
guard atom._loader.shouldUpdate(newValue: newValue, oldValue: oldValue) else {
371350
return
372351
}
373352

374-
// Notifying update to view subscriptions first.
375-
if let subscriptions = store.state.subscriptions[key] {
376-
for subscription in ContiguousArray(subscriptions.values) {
377-
if case .objectWillChange = order, subscription.requiresObjectUpdate {
378-
RunLoop.current.perform(subscription.notifyUpdate)
379-
}
380-
else {
381-
subscription.notifyUpdate()
353+
atom._loader.performUpdate {
354+
// Notifies update to view subscriptions first.
355+
if let subscriptions = store.state.subscriptions[key] {
356+
for subscription in ContiguousArray(subscriptions.values) {
357+
subscription.update()
382358
}
383359
}
384-
}
385360

386-
func notifyUpdate() {
361+
// Notifies update to downstream atoms.
387362
if let children = store.graph.children[key] {
388363
for child in ContiguousArray(children) {
389364
// Reset the atom value and then notifies downstream atoms.
@@ -398,21 +373,7 @@ private extension StoreContext {
398373

399374
let state = getState(of: atom, for: key)
400375
let context = AtomCurrentContext(store: self, coordinator: state.coordinator)
401-
atom.updated(newValue: value, oldValue: oldValue, context: context)
402-
}
403-
404-
switch order {
405-
case .newValue:
406-
notifyUpdate()
407-
408-
case .objectWillChange:
409-
// At the timing when `ObservableObject/objectWillChange` emits, its properties
410-
// have not yet been updated and are still old when dependent atoms read it.
411-
// As a workaround, the update is executed in the next run loop
412-
// so that the downstream atoms can receive the object that's already updated.
413-
RunLoop.current.perform {
414-
notifyUpdate()
415-
}
376+
atom.updated(newValue: newValue, oldValue: oldValue, context: context)
416377
}
417378
}
418379

Sources/Atoms/Core/Subscriber.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ internal struct Subscriber {
44
private weak var state: SubscriberState?
55

66
let key: SubscriberKey
7-
let location: SourceLocation
87

9-
init(_ state: SubscriberState, location: SourceLocation) {
8+
init(_ state: SubscriberState) {
109
self.state = state
1110
self.key = SubscriberKey(token: state.token)
12-
self.location = location
1311
}
1412

1513
var subscribingKeys: Set<AtomKey> {
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
@usableFromInline
12
@MainActor
23
internal struct Subscription {
34
let location: SourceLocation
4-
let requiresObjectUpdate: Bool
5-
let notifyUpdate: () -> Void
5+
let update: () -> Void
66
}

0 commit comments

Comments
 (0)