Skip to content

Commit a81212f

Browse files
authored
feat: Atom Coordinator API (#24)
1 parent df7d6cc commit a81212f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+559
-466
lines changed

Examples/Packages/iOS/Sources/ExampleMap/Atoms.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,6 @@ final class LocationObserver: NSObject, ObservableObject, CLLocationManagerDeleg
1414
manager.delegate = self
1515
}
1616

17-
convenience override init() {
18-
let manager = CLLocationManager()
19-
manager.desiredAccuracy = kCLLocationAccuracyBest
20-
21-
self.init(manager: manager)
22-
}
23-
2417
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
2518
objectWillChange.send()
2619

@@ -45,8 +38,14 @@ final class LocationObserver: NSObject, ObservableObject, CLLocationManagerDeleg
4538
}
4639

4740
struct LocationObserverAtom: ObservableObjectAtom, Hashable {
41+
func makeCoordinator() -> LocationManagerProtocol {
42+
let manager = CLLocationManager()
43+
manager.desiredAccuracy = kCLLocationAccuracyBest
44+
return manager
45+
}
46+
4847
func object(context: Context) -> LocationObserver {
49-
LocationObserver()
48+
LocationObserver(manager: context.coordinator)
5049
}
5150
}
5251

README.md

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
- [Atoms](#atoms)
2424
- [Modifiers](#modifiers)
2525
- [Property Wrappers](#property-wrappers)
26-
- [Contexts](#contexts)
26+
- [Context](#context)
2727
- [KeepAlive](#keepalive)
2828
- [Suspense](#suspense)
2929
- [Testing](#testing)
@@ -903,13 +903,13 @@ await context.refresh(FetchMoviesAtom())
903903
context.reset(CounterAtom())
904904
```
905905

906-
The context also provides a flexible solution for passing dynamic parameters to atom's initializer. See [Contexts](#contexts) section for more detail.
906+
The context also provides a flexible solution for passing dynamic parameters to atom's initializer. See [Context](#context) section for more detail.
907907

908908
---
909909

910-
### Contexts
910+
### Context
911911

912-
Contexts are context structure for using and interacting with the data of other atoms from a view or an another atom. The basic API common to all contexts is as follows:
912+
Context is a structure for using and interacting with atom values from views or other atoms.
913913

914914
|API|Use|
915915
|:--|:--|
@@ -921,7 +921,7 @@ Contexts are context structure for using and interacting with the data of other
921921
|[refresh(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/refresh(_:))|Reset an atom and await until asynchronous operation is complete.|
922922
|[reset(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/reset(_:))|Reset an atom to the default value or a first output.|
923923

924-
There are the following types context as different contextual environments, and they have some specific APIs for each.
924+
There are the following types context as different contextual environments.
925925

926926
#### [AtomViewContext](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomviewcontext)
927927

@@ -992,24 +992,23 @@ struct BooksView: View {
992992

993993
</details>
994994

995-
Context available through the `@ViewContext` property wrapper when using atoms from a view. There is no specific API for this context.
995+
A context available through the `@ViewContext` property wrapper when using atoms from a view.
996996

997997
#### [AtomTransactionContext](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomtransactioncontext)
998998

999999
<details><summary><code>📖 Click to expand example code</code></summary>
10001000

10011001
```swift
1002-
class LocationManagerDelegate: NSObject, CLLocationManagerDelegate { ... }
1003-
10041002
struct LocationManagerAtom: ValueAtom, Hashable {
1005-
func value(context: Context) -> LocationManagerProtocol {
1006-
let manager = CLLocationManager()
1007-
let delegate = LocationManagerDelegate()
1003+
final class Coordinator: NSObject, CLLocationManagerDelegate { ... }
10081004

1009-
manager.delegate = delegate
1010-
context.addTermination(manager.stopUpdatingLocation)
1011-
context.keepUntilTermination(delegate)
1005+
func makeCoordinator() -> Coordinator {
1006+
Coordinator()
1007+
}
10121008

1009+
func value(context: Context) -> LocationManagerProtocol {
1010+
let manager = CLLocationManager()
1011+
manager.delegate = context.coordinator
10131012
return manager
10141013
}
10151014
}
@@ -1024,12 +1023,12 @@ struct CoordinateAtom: ValueAtom, Hashable {
10241023

10251024
</details>
10261025

1027-
Context passed as a parameter to the primary function of each atom type.
1026+
A context passed as a parameter to the primary function of each atom type.
1027+
This context type has a `coordinator` property that preserves an instance from the time an atom is used and initialized until it is unused and cleaned up, so it can be used to cache values or as a lifecycle for an atom.
10281028

10291029
|API|Use|
10301030
|:--|:--|
1031-
|[addTermination(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomtransactioncontext/addtermination(_:))|Calls the passed closure when the atom is updated or is no longer used.|
1032-
|[keepUntilTermination(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomtransactioncontext/keepuntiltermination(_:))|Retains the given object instance until the atom is updated or is no longer used.|
1031+
|[coordinator](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomtransactioncontext/coordinator)|The atom’s associated coordinator that preservess a state until the atom will no longer be used.|
10331032

10341033
#### [AtomTestContext](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomtestcontext)
10351034

@@ -1074,7 +1073,7 @@ class FetchMusicsTests: XCTestCase {
10741073

10751074
</details>
10761075

1077-
Context that can simulate any scenarios in which atoms are used from a view or another atom and provides a comprehensive means of testing.
1076+
A context that can simulate any scenarios in which atoms are used from a view or another atom and provides a comprehensive means of testing.
10781077

10791078
|API|Use|
10801079
|:--|:--|
@@ -1133,7 +1132,7 @@ In order to fully test your app, this library guarantees the following principle
11331132
- Dependencies are replaceable with any of mock/stub/fake/spy per test case.
11341133
- Test cases can reproduce any possible scenarios at the view-layer.
11351134

1136-
In the test case, you first create an `AtomTestContext` instance that behaves similarly to other context types. The context allows for flexible reproduction of expected scenarios for testing using the control functions described in the [Contexts](#contexts) section.
1135+
In the test case, you first create an `AtomTestContext` instance that behaves similarly to other context types. The context allows for flexible reproduction of expected scenarios for testing using the control functions described in the [Context](#context) section.
11371136
In addition, it's able to replace the atom value with test-friendly dependencies with `override` function. It helps you to write a reproducible & stable testing.
11381137
Since atom needs to be used from the main actor to guarantee thread-safety, `XCTestCase` class that to test atoms should have `@MainActor` attribute.
11391138

Sources/Atoms/Atom/AsyncSequenceAtom.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
/// }
4545
/// ```
4646
///
47-
public protocol AsyncSequenceAtom: Atom where Loader == AsyncSequenceAtomLoader<Sequence> {
47+
public protocol AsyncSequenceAtom: Atom {
4848
/// The type of asynchronous sequence that this atom manages.
4949
associatedtype Sequence: AsyncSequence
5050

@@ -64,7 +64,7 @@ public protocol AsyncSequenceAtom: Atom where Loader == AsyncSequenceAtomLoader<
6464

6565
public extension AsyncSequenceAtom {
6666
@MainActor
67-
var _loader: Loader {
68-
Loader(makeSequence: sequence)
67+
var _loader: AsyncSequenceAtomLoader<Self> {
68+
AsyncSequenceAtomLoader(atom: self)
6969
}
7070
}

Sources/Atoms/Atom/Atom.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ public protocol Atom {
1010
associatedtype Key: Hashable
1111

1212
/// A loader type that represents an actual implementation of the corresponding atom.
13-
associatedtype Loader: AtomLoader
13+
associatedtype Loader: AtomLoader where Loader.Coordinator == Coordinator
14+
15+
/// A type to coordinate with the atom.
16+
associatedtype Coordinator = Void
1417

1518
/// A type of the context structure that to read, watch, and otherwise interacting
1619
/// with other atoms.
17-
typealias Context = AtomTransactionContext
20+
typealias Context = AtomTransactionContext<Coordinator>
1821

1922
/// A boolean value indicating whether the atom value should be preserved even if
2023
/// no longer watched to.
@@ -31,6 +34,17 @@ public protocol Atom {
3134
/// If this atom conforms to `Hashable`, it will adopt itself as the `key` by default.
3235
var key: Key { get }
3336

37+
/// Creates the custom coordinator instance that you use to preserve arbitrary state of
38+
/// the atom.
39+
///
40+
/// It's called when the atom is initialized, and the same instance is preserved until
41+
/// the atom is no longer used and is deinitialized.
42+
///
43+
/// - Returns: The atom's associated coordinator instance.
44+
func makeCoordinator() -> Coordinator
45+
46+
// --- Internal ---
47+
3448
/// A loader that represents an actual implementation of the corresponding atom.
3549
@MainActor
3650
var _loader: Loader { get }
@@ -40,8 +54,14 @@ public extension Atom {
4054
static var shouldKeepAlive: Bool {
4155
false
4256
}
57+
58+
func makeCoordinator() -> Coordinator where Coordinator == Void {
59+
()
60+
}
4361
}
4462

4563
public extension Atom where Self == Key {
46-
var key: Self { self }
64+
var key: Self {
65+
self
66+
}
4767
}

Sources/Atoms/Atom/ObservableObjectAtom.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import Combine
4848
/// }
4949
/// ```
5050
///
51-
public protocol ObservableObjectAtom: Atom where Loader == ObservableObjectAtomLoader<ObjectType> {
51+
public protocol ObservableObjectAtom: Atom {
5252
/// The type of observable object that this atom produces.
5353
associatedtype ObjectType: ObservableObject
5454

@@ -67,7 +67,7 @@ public protocol ObservableObjectAtom: Atom where Loader == ObservableObjectAtomL
6767

6868
public extension ObservableObjectAtom {
6969
@MainActor
70-
var _loader: Loader {
71-
Loader(makeObject: object)
70+
var _loader: ObservableObjectAtomLoader<Self> {
71+
ObservableObjectAtomLoader(atom: self)
7272
}
7373
}

Sources/Atoms/Atom/PublisherAtom.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import Combine
3737
/// }
3838
/// ```
3939
///
40-
public protocol PublisherAtom: Atom where Loader == PublisherAtomLoader<Publisher> {
40+
public protocol PublisherAtom: Atom {
4141
/// The type of publisher that this atom manages.
4242
associatedtype Publisher: Combine.Publisher
4343

@@ -57,7 +57,7 @@ public protocol PublisherAtom: Atom where Loader == PublisherAtomLoader<Publishe
5757

5858
public extension PublisherAtom {
5959
@MainActor
60-
var _loader: Loader {
61-
Loader(makePublisher: publisher)
60+
var _loader: PublisherAtomLoader<Self> {
61+
PublisherAtomLoader(atom: self)
6262
}
6363
}

Sources/Atoms/Atom/StateAtom.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
/// }
3737
/// ```
3838
///
39-
public protocol StateAtom: Atom where Loader == ValueAtomLoader<Value> {
39+
public protocol StateAtom: Atom {
4040
/// The type of state value that this atom produces.
4141
associatedtype Value
4242

@@ -61,7 +61,7 @@ public protocol StateAtom: Atom where Loader == ValueAtomLoader<Value> {
6161
/// - context: A context structure that to read, watch, and otherwise
6262
/// interacting with other atoms.
6363
@MainActor
64-
func willSet(newValue: Value, oldValue: Value, context: Context)
64+
func willSet(newValue: Loader.Value, oldValue: Loader.Value, context: Context)
6565

6666
/// Observes and responds to changes in the state value which is called just after
6767
/// the state is changed.
@@ -72,18 +72,18 @@ public protocol StateAtom: Atom where Loader == ValueAtomLoader<Value> {
7272
/// - context: A context structure that to read, watch, and otherwise
7373
/// interacting with other atoms.
7474
@MainActor
75-
func didSet(newValue: Value, oldValue: Value, context: Context)
75+
func didSet(newValue: Loader.Value, oldValue: Loader.Value, context: Context)
7676
}
7777

7878
public extension StateAtom {
7979
@MainActor
80-
var _loader: Loader {
81-
Loader(getValue: defaultValue)
80+
var _loader: StateAtomLoader<Self> {
81+
StateAtomLoader(atom: self)
8282
}
8383

8484
@MainActor
85-
func willSet(newValue: Value, oldValue: Value, context: Context) {}
85+
func willSet(newValue: Loader.Value, oldValue: Loader.Value, context: Context) {}
8686

8787
@MainActor
88-
func didSet(newValue: Value, oldValue: Value, context: Context) {}
88+
func didSet(newValue: Loader.Value, oldValue: Loader.Value, context: Context) {}
8989
}

Sources/Atoms/Atom/TaskAtom.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
/// }
3535
/// ```
3636
///
37-
public protocol TaskAtom: Atom where Loader == TaskAtomLoader<Value> {
37+
public protocol TaskAtom: Atom {
3838
/// The type of value that this atom produces.
3939
associatedtype Value
4040

@@ -53,7 +53,7 @@ public protocol TaskAtom: Atom where Loader == TaskAtomLoader<Value> {
5353

5454
public extension TaskAtom {
5555
@MainActor
56-
var _loader: Loader {
57-
Loader(getValue: value)
56+
var _loader: TaskAtomLoader<Self> {
57+
TaskAtomLoader(atom: self)
5858
}
5959
}

Sources/Atoms/Atom/ThrowingTaskAtom.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
/// }
3737
/// ```
3838
///
39-
public protocol ThrowingTaskAtom: Atom where Loader == ThrowingTaskAtomLoader<Value> {
39+
public protocol ThrowingTaskAtom: Atom {
4040
/// The type of value that this atom produces.
4141
associatedtype Value
4242

@@ -57,7 +57,7 @@ public protocol ThrowingTaskAtom: Atom where Loader == ThrowingTaskAtomLoader<Va
5757

5858
public extension ThrowingTaskAtom {
5959
@MainActor
60-
var _loader: Loader {
61-
Loader(getValue: value)
60+
var _loader: ThrowingTaskAtomLoader<Self> {
61+
ThrowingTaskAtomLoader(atom: self)
6262
}
6363
}

Sources/Atoms/Atom/ValueAtom.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
/// }
3030
/// ```
3131
///
32-
public protocol ValueAtom: Atom where Loader == ValueAtomLoader<Value> {
32+
public protocol ValueAtom: Atom {
3333
/// The type of value that this atom produces.
3434
associatedtype Value
3535

@@ -48,7 +48,7 @@ public protocol ValueAtom: Atom where Loader == ValueAtomLoader<Value> {
4848

4949
public extension ValueAtom {
5050
@MainActor
51-
var _loader: Loader {
52-
Loader(getValue: value)
51+
var _loader: ValueAtomLoader<Self> {
52+
ValueAtomLoader(atom: self)
5353
}
5454
}

0 commit comments

Comments
 (0)