diff --git a/Sources/Defaults/Defaults.swift b/Sources/Defaults/Defaults.swift index 01a16de..6dc9a25 100644 --- a/Sources/Defaults/Defaults.swift +++ b/Sources/Defaults/Defaults.swift @@ -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( + _ keys: repeat Key, + 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 + 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. + 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]) { @@ -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], diff --git a/Sources/Defaults/Documentation.docc/Documentation.md b/Sources/Defaults/Documentation.docc/Documentation.md index 0ba7818..0760dbc 100644 --- a/Sources/Defaults/Documentation.docc/Documentation.md +++ b/Sources/Defaults/Documentation.docc/Documentation.md @@ -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`` diff --git a/Tests/DefaultsTests/DefaultsTests.swift b/Tests/DefaultsTests/DefaultsTests.swift index 3c7d46b..ae53489 100644 --- a/Tests/DefaultsTests/DefaultsTests.swift +++ b/Tests/DefaultsTests/DefaultsTests.swift @@ -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("updatesMultipleKeyVariadic1", default: false, suite: suite_) + let key2 = Defaults.Key("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 {