Skip to content

Commit c65e5b0

Browse files
committed
Add documentation
1 parent 55e9813 commit c65e5b0

File tree

3 files changed

+153
-8
lines changed

3 files changed

+153
-8
lines changed

README.md

+128
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,130 @@
11
# AsyncObservable
22

3+
Some of the features that Combine used to offer, but using Swift concurrency and @Observable instead. So it's more compatible with modern setups and should work just fine on any platform.
4+
Designed for Swift 6.
5+
6+
7+
A single property that is thread safe and can be observed using async streams or @Observable.
8+
9+
```swift
10+
import AsyncObservable
11+
12+
actor Something {
13+
let someProperty = AsyncObservable("Hello, world!")
14+
15+
func funcThatUpdatesProperty() async {
16+
await someProperty.update("Hello, world! 2")
17+
}
18+
}
19+
20+
let something = Something()
21+
something.someProperty.value // "Hello, world!"
22+
23+
for await value in something.someProperty.valueStream {
24+
print(value) // hello world (then whatever the property is updated to)
25+
}
26+
27+
28+
struct SomethingView: View {
29+
let something: Something // Note: someProperty should be marked with @MainActor for this to work as is
30+
var body: some View {
31+
Text(something.someProperty.valueObservable) // hello world (then whatever the property is updated to)
32+
}
33+
}
34+
```
35+
36+
37+
## Stream
38+
39+
The streams buffering policy defaults to `.unbounded`, so it will "gather" values as soon as you create it.
40+
41+
```swift
42+
let someProperty = AsyncObservable(1)
43+
44+
let stream = someProperty.valueStream // already has 1
45+
someProperty.update { $0 + 1 } // 2
46+
someProperty.update { $0 + 1 } // 3
47+
someProperty.update { $0 + 1 } // 4
48+
49+
for await value in stream {
50+
print(value) // 1, 2, 3, 4
51+
}
52+
```
53+
54+
Canceling the task that the stream is running in will cancel the stream. So you don't need to have manual `if Task.isCancelled` checks. But you can still check it if you want.
55+
56+
```swift
57+
let someProperty = AsyncObservable(1)
58+
59+
let stream = someProperty.valueStream // already has 1
60+
let task = Task {
61+
for await value in stream {
62+
print(value) // 1, 2, 3
63+
}
64+
}
65+
66+
67+
someProperty.update { $0 + 1 } // 2
68+
someProperty.update { $0 + 1 } // 3
69+
task.cancel()
70+
someProperty.update { $0 + 1 } // 4
71+
```
72+
73+
Streams are finalized as soon as you break out of the loop, so you can't reuse them. But you can create as many new ones as you like.
74+
75+
```swift
76+
let someProperty = AsyncObservable(1)
77+
78+
let stream = someProperty.valueStream // already has 1
79+
// only print first value
80+
for await value in stream {
81+
print(value) // 1
82+
break
83+
}
84+
85+
// don't do this ❌
86+
// the stream is already finalized
87+
for await value in stream {
88+
}
89+
90+
// do this ✅
91+
for await value in someProperty.valueStream {
92+
93+
}
94+
```
95+
96+
## Mutate
97+
98+
Sometimes you just want to mutate the original value instead of having to copy and return a new value. This still updates all the observers correctly and is safe.
99+
100+
```swift
101+
let values = AsyncObservable([1, 2, 3])
102+
103+
values.mutate { $0.append(4) }
104+
```
105+
106+
107+
## Buffering Policy
108+
109+
The buffering policy defaults to `.unbounded`, but you can change it on init.
110+
111+
```swift
112+
let someProperty = AsyncObservable("Hello, world!", bufferingPolicy: .bufferingNewest(1))
113+
```
114+
115+
## DispatchQueue
116+
117+
You can pass a custom dispatch queue to the initializer, just make sure it's a serial queue. Don't change the queue unless you really need to.
118+
119+
```swift
120+
let someProperty = AsyncObservable("Hello, world!", dispatchQueue: DispatchSerialQueue(label: "SomeQueue"))
121+
```
122+
123+
## UserDefaults
124+
125+
Use the `AsyncObservableUserDefaults` class to store values in UserDefaults. Works just the same as `AsyncObservable`, but automatically saves to UserDefaults and loads from there.
126+
127+
```swift
128+
let someProperty = AsyncObservableUserDefaults("someKey", initialValue: "Hello, world!")
129+
```
130+

Sources/AsyncObservable/AsyncObservable.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ open class AsyncObservable<T: Sendable>: @unchecked Sendable {
4949
@MainActor
5050
public class State {
5151
/// The current value, only settable internally but observable externally
52-
public internal(set) var value: T {
52+
public var value: T {
5353
didSet {
5454
didSetValue(value)
5555
}

Sources/AsyncObservableUserDefaults/AsyncObservableUserDefaults.swift

+24-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import Foundation
33
/// A specialized version of AsyncObservable that persists its value to UserDefaults.
44
/// The managed value must conform to both Sendable and Codable protocols.
55
///
6+
/// This class automatically:
7+
/// - Loads the initial value from UserDefaults if available
8+
/// - Persists value changes to UserDefaults
9+
/// - Maintains all the async stream and observable functionality of AsyncObservable
10+
///
611
/// Example usage:
712
/// ```swift
813
/// // Create a persistent manager
@@ -12,18 +17,31 @@ import Foundation
1217
/// )
1318
///
1419
/// // Value will be automatically saved to UserDefaults on updates
15-
/// await manager.update(42)
20+
/// manager.update(42)
1621
///
1722
/// // Remove the value from UserDefaults
1823
/// manager.remove()
1924
/// ```
2025
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
2126
public class AsyncObservableUserDefaults<T: Sendable & Codable>: AsyncObservable<T>, @unchecked Sendable {
27+
/// The UserDefaults instance used for persistence
2228
public let userDefaults: UserDefaults
29+
30+
/// The key used to store the value in UserDefaults
2331
public let key: String
2432

33+
/// Creates a new AsyncObservableUserDefaults instance.
34+
///
35+
/// - Parameters:
36+
/// - key: The key to use for storing the value in UserDefaults
37+
/// - initialValue: The initial value to use if no value is found in UserDefaults
38+
/// - userDefaults: The UserDefaults instance to use (default: .standard)
39+
/// - serialQueue: The dispatch queue used for synchronization (default: new serial queue)
2540
public init(
26-
key: String, initialValue: T, userDefaults: UserDefaults = .standard, serialQueue: DispatchQueue = DispatchSerialQueue(label: "AsyncObservable")
41+
key: String,
42+
initialValue: T,
43+
userDefaults: UserDefaults = .standard,
44+
serialQueue: DispatchQueue = DispatchSerialQueue(label: "AsyncObservable")
2745
) {
2846
var _initialValue = initialValue
2947
self.userDefaults = userDefaults
@@ -35,18 +53,17 @@ public class AsyncObservableUserDefaults<T: Sendable & Codable>: AsyncObservable
3553
super.init(_initialValue, serialQueue: serialQueue)
3654
}
3755

38-
/// Task that synchronizes the observable state with the current value
39-
private var updateStateTask: Task<Void, Never>?
40-
41-
/// Sets up the task that keeps the observable state in sync with the current value
56+
/// Updates the observable value and persists the change to UserDefaults.
57+
///
58+
/// - Parameter value: The new value to set in the observable state and persist
4259
override open func updateObservableValue(_ value: T) {
4360
super.updateObservableValue(value)
4461
save(value, forKey: key)
4562
}
4663

4764
/// Removes the stored value from UserDefaults.
4865
/// This does not affect the current in-memory value.
49-
func remove() {
66+
public func remove() {
5067
userDefaults.removeObject(forKey: key)
5168
}
5269

0 commit comments

Comments
 (0)