Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add variadic generics version of Defaults.updates() #191

Merged
merged 1 commit into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions Sources/Defaults/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -292,13 +292,64 @@ extension Defaults {
}
}

// We still keep this as it can be useful to pass a dynamic array of keys.
/**
Observe updates to multiple stored values.

- Parameter keys: The keys to observe updates from.
- Parameter initial: Trigger an initial event on creation. This can be useful for setting default values on controls.

```swift
Task {
for await (foo, bar) in Defaults.updates([.foo, .bar]) {
print("Values changed:", foo, bar)
}
}
```
*/
public static func updates<each Value: Serializable>(
_ keys: repeat Key<each Value>,
initial: Bool = true
) -> AsyncStream<(repeat each Value)> {
.init { continuation in
func getCurrentValues() -> (repeat each Value) {
(repeat self[each keys])
}

var observations = [DefaultsObservation]()

if initial {
continuation.yield(getCurrentValues())
}

for key in repeat (each keys) {
let observation = DefaultsObservation(object: key.suite, key: key.name) { _, _ in
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I'm intentionally not using CompositeDefaultsObservation for simplicity, as it's not really needed here.

continuation.yield(getCurrentValues())
}

observation.start(options: [])
observations.append(observation)
}

let immutableObservations = observations

continuation.onTermination = { _ in
// `invalidate()` should be thread-safe, but it is not in practice.
Copy link
Collaborator

@hank121314 hank121314 Oct 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about this. I thought that invalidate() would be thread-safe with Lock,
Should we use DispatchQueue.sync instead inside the DefaultsObservation?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure. I have seen KVO crashes in some of my apps that may have been caused by some threading problems in macOS. I prefer to keep it limited to this code until I can 100% confirm it. This is just a precaution.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it! Thanks for the explanation!

DispatchQueue.main.async {
for observation in immutableObservations {
observation.invalidate()
}
}
}
}
}

// We still keep this as it can be useful to pass a dynamic array of keys.
/**
Observe updates to multiple stored values without receiving the values.

- Parameter keys: The keys to observe updates from.
- Parameter initial: Trigger an initial event on creation. This can be useful for setting default values on controls.

```swift
Task {
for await _ in Defaults.updates([.foo, .bar]) {
Expand All @@ -307,7 +358,7 @@ extension Defaults {
}
```

- Note: This does not include which of the values changed. Use ``Defaults/updates(_:initial:)-88orv`` if you need that. You could use [`merge`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md) to merge them into a single sequence.
- Note: This does not include which of the values changed. Use ``Defaults/updates(_:initial:)-l03o`` if you need that.
*/
public static func updates(
_ keys: [_AnyKey],
Expand Down
1 change: 1 addition & 0 deletions Sources/Defaults/Documentation.docc/Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ typealias Default = _Default
### Methods

- ``Defaults/updates(_:initial:)-88orv``
- ``Defaults/updates(_:initial:)-l03o``
- ``Defaults/updates(_:initial:)-1mqkb``
- ``Defaults/reset(_:)-7jv5v``
- ``Defaults/reset(_:)-7es1e``
Expand Down
28 changes: 28 additions & 0 deletions Tests/DefaultsTests/DefaultsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,34 @@ final class DefaultsTests {
let count = await counter.count
#expect(count == 2)
}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *)
@Test
func testUpdatesMultipleKeysVariadic() async {
let key1 = Defaults.Key<Bool>("updatesMultipleKeyVariadic1", default: false, suite: suite_)
let key2 = Defaults.Key<Bool>("updatesMultipleKeyVariadic2", default: false, suite: suite_)
let counter = Counter()

async let waiter: Void = {
for await (_, _) in Defaults.updates(key1, key2, initial: false) {
await counter.increment()

if await counter.count == 2 {
break
}
}
}()

try? await Task.sleep(for: .seconds(0.1))

Defaults[key1] = true
Defaults[key2] = true

await waiter

let count = await counter.count
#expect(count == 2)
}
}

actor Counter {
Expand Down