Skip to content

Commit ac9901e

Browse files
committed
Implement animation modifier
1 parent 7dc2744 commit ac9901e

23 files changed

+258
-169
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: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ public struct AtomLoaderContext<Value, Coordinator> {
44
internal let store: StoreContext
55
internal let transaction: Transaction
66
internal let coordinator: Coordinator
7-
internal let update: @MainActor (Value, UpdateOrder) -> Void
7+
internal 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
@@ -24,8 +24,8 @@ public struct AtomLoaderContext<Value, Coordinator> {
2424
}
2525
}
2626

27-
internal func update(with value: Value, order: UpdateOrder = .newValue) {
28-
update(value, order)
27+
internal func update(with value: Value) {
28+
update(value)
2929
}
3030

3131
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: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@ 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 { _ 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+
context.update(with: value)
33+
}
2934
}
3035

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

3638
return value

Sources/Atoms/Core/StoreContext.swift

Lines changed: 23 additions & 61 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

@@ -103,7 +101,7 @@ internal struct StoreContext {
103101
if let cache = lookupCache(of: atom, for: key) {
104102
var value = cache.value
105103
body(&value)
106-
update(atom: atom, for: key, value: value, cache: cache, order: .newValue)
104+
update(atom: atom, for: key, newValue: value, cache: cache)
107105
}
108106
}
109107

@@ -129,18 +127,12 @@ internal struct StoreContext {
129127
func watch<Node: Atom>(
130128
_ atom: Node,
131129
subscriber: Subscriber,
132-
requiresObjectUpdate: Bool,
133-
notifyUpdate: @escaping () -> Void
130+
subscription: Subscription
134131
) -> Node.Loader.Value {
135132
let override = lookupOverride(of: atom)
136133
let scopeKey = lookupScopeKey(of: atom, isScopedOverriden: override?.isScoped ?? false)
137134
let key = AtomKey(atom, scopeKey: scopeKey)
138135
let cache = getCache(of: atom, for: key, override: override)
139-
let subscription = Subscription(
140-
location: subscriber.location,
141-
requiresObjectUpdate: requiresObjectUpdate,
142-
notifyUpdate: notifyUpdate
143-
)
144136
let isNewSubscription = subscriber.subscribingKeys.insert(key).inserted
145137

146138
store.state.subscriptions[key, default: [:]].updateValue(subscription, forKey: subscriber.key)
@@ -178,7 +170,7 @@ internal struct StoreContext {
178170

179171
// Notify update unless it's cancelled or terminated by other operations.
180172
if !Task.isCancelled && !context.transaction.isTerminated {
181-
update(atom: atom, for: key, value: value, cache: cache, order: .newValue)
173+
update(atom: atom, for: key, newValue: value, cache: cache)
182174
}
183175

184176
return value
@@ -199,7 +191,7 @@ internal struct StoreContext {
199191

200192
// Notify update unless it's cancelled or terminated by other operations.
201193
if !Task.isCancelled && !transaction.isTerminated {
202-
update(atom: atom, for: key, value: value, cache: cache, order: .newValue)
194+
update(atom: atom, for: key, newValue: value, cache: cache)
203195
}
204196

205197
return value
@@ -214,7 +206,7 @@ internal struct StoreContext {
214206

215207
if let cache = lookupCache(of: atom, for: key) {
216208
let newCache = makeCache(of: atom, for: key, override: override)
217-
update(atom: atom, for: key, value: newCache.value, cache: cache, order: .newValue)
209+
update(atom: atom, for: key, newValue: newCache.value, cache: cache)
218210
}
219211
}
220212

@@ -289,7 +281,7 @@ internal struct StoreContext {
289281
// Notify updates only for the subscriptions of restored atoms.
290282
if let subscriptions = store.state.subscriptions[key] {
291283
for subscription in ContiguousArray(subscriptions.values) {
292-
subscription.notifyUpdate()
284+
subscription.update()
293285
}
294286
}
295287
}
@@ -336,54 +328,38 @@ private extension StoreContext {
336328
store: self,
337329
transaction: transaction,
338330
coordinator: state.coordinator
339-
) { value, order in
340-
guard let cache = lookupCache(of: atom, for: key) else {
341-
return
331+
) { newValue in
332+
if let cache = lookupCache(of: atom, for: key) {
333+
update(atom: atom, for: key, newValue: newValue, cache: cache)
342334
}
343-
344-
update(
345-
atom: atom,
346-
for: key,
347-
value: value,
348-
cache: cache,
349-
order: order
350-
)
351335
}
352336
}
353337

354338
func update<Node: Atom>(
355339
atom: Node,
356340
for key: AtomKey,
357-
value: Node.Loader.Value,
358-
cache: AtomCache<Node>,
359-
order: UpdateOrder
341+
newValue: Node.Loader.Value,
342+
cache: AtomCache<Node>
360343
) {
361344
let oldValue = cache.value
345+
var cache = cache
362346

363-
if case .newValue = order {
364-
var cache = cache
365-
cache.value = value
366-
store.state.caches[key] = cache
367-
}
347+
cache.value = newValue
348+
store.state.caches[key] = cache
368349

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

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()
354+
atom._loader.performUpdate {
355+
// Notifies update to view subscriptions first.
356+
if let subscriptions = store.state.subscriptions[key] {
357+
for subscription in ContiguousArray(subscriptions.values) {
358+
subscription.update()
382359
}
383360
}
384-
}
385361

386-
func notifyUpdate() {
362+
// Notifies update to downstream atoms.
387363
if let children = store.graph.children[key] {
388364
for child in ContiguousArray(children) {
389365
// Reset the atom value and then notifies downstream atoms.
@@ -398,21 +374,7 @@ private extension StoreContext {
398374

399375
let state = getState(of: atom, for: key)
400376
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-
}
377+
atom.updated(newValue: newValue, oldValue: oldValue, context: context)
416378
}
417379
}
418380

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)