Skip to content

Commit

Permalink
Add .updates() method
Browse files Browse the repository at this point in the history
Fixes #77
  • Loading branch information
sindresorhus committed Nov 19, 2022
1 parent ea11b7a commit 7a22d37
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 206 deletions.
88 changes: 84 additions & 4 deletions Sources/Defaults/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ extension Defaults {
}
```
- Warning: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`).
- Warning: The `UserDefaults` name must be ASCII, not start with `@`, and cannot contain a dot (`.`).
*/
public final class Key<Value: Serializable>: _AnyKey {
/**
Expand All @@ -90,7 +90,7 @@ extension Defaults {
public var defaultValue: Value { defaultValueGetter() }

/**
Create a defaults key.
Create a key.
- Parameter key: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`).
Expand Down Expand Up @@ -118,7 +118,7 @@ extension Defaults {
}

/**
Create a defaults key with a dynamic default value.
Create a key with a dynamic default value.
This can be useful in cases where you cannot define a static default value as it may change during the lifetime of the app.
Expand All @@ -143,7 +143,7 @@ extension Defaults {
}

/**
Create a defaults key with an optional value.
Create a key with an optional value.
- Parameter key: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`).
*/
Expand Down Expand Up @@ -286,3 +286,83 @@ extension Defaults {
*/
typealias CodableBridge = _DefaultsCodableBridge
}

extension Defaults {
/**
Observe updates to a stored value.
- Parameter initial: Trigger an initial event on creation. This can be useful for setting default values on controls.
```swift
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
// …
Task {
for await value in Defaults.updates(.isUnicornMode) {
print("Value:", value)
}
}
```
*/
public static func updates<Value: Serializable>(
_ key: Key<Value>,
initial: Bool = true
) -> AsyncStream<Value> { // TODO: Make this `some AsyncSequence<Value>` when Swift 6 is out.
.init { continuation in
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
// TODO: Use the `.deserialize` method directly.
let value = KeyChange(change: change, defaultValue: key.defaultValue).newValue
continuation.yield(value)
}

observation.start(options: initial ? [.initial] : [])

continuation.onTermination = { _ in
observation.invalidate()
}
}
}

// TODO: Make this include a tuple with the values when Swift supports variadic generics. I can then simply use `merge()` with the first `updates()` method.
/**
Observe updates to multiple stored values.
- 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]) {
print("One of the values changed")
}
}
```
- Note: This does not include which of the values changed. Use ``Defaults/updates(_:initial:)-9eh8`` 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.
*/
public static func updates(
_ keys: [_AnyKey],
initial: Bool = true
) -> AsyncStream<Void> { // TODO: Make this `some AsyncSequence<Value>` when Swift 6 is out.
.init { continuation in
let observations = keys.indexed().map { index, key in
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { _ in
continuation.yield()
}

// Ensure we only trigger a single initial event.
observation.start(options: initial && index == 0 ? [.initial] : [])

return observation
}

continuation.onTermination = { _ in
for observation in observations {
observation.invalidate()
}
}
}
}
}
7 changes: 2 additions & 5 deletions Sources/Defaults/Documentation.docc/Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Defaults[.quality] = 0.5

### Methods

- ``Defaults/updates(_:initial:)-9eh8``
- ``Defaults/updates(_:initial:)-1mqkb``
- ``Defaults/reset(_:)-7jv5v``
- ``Defaults/reset(_:)-7es1e``
- ``Defaults/removeAll(suite:)``
Expand All @@ -49,11 +51,6 @@ Defaults[.quality] = 0.5
- ``Default``
- ``Defaults/Toggle``

### Events

- ``Defaults/publisher(_:options:)``
- ``Defaults/publisher(keys:options:)``

### Force Type Resolution

- ``Defaults/PreferRawRepresentable``
Expand Down
6 changes: 4 additions & 2 deletions Sources/Defaults/Observation+Combine.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#if canImport(Combine)
import Foundation
import Combine

Expand Down Expand Up @@ -84,6 +83,8 @@ extension Defaults {
//=> false
}
```
- Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead.
*/
public static func publisher<Value: Serializable>(
_ key: Key<Value>,
Expand All @@ -97,6 +98,8 @@ extension Defaults {

/**
Publisher for multiple `Key<T>` observation, but without specific information about changes.
- Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead.
*/
public static func publisher(
keys: _AnyKey...,
Expand All @@ -118,4 +121,3 @@ extension Defaults {
return combinedPublisher
}
}
#endif
18 changes: 11 additions & 7 deletions Sources/Defaults/Observation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,15 @@ extension Defaults {
object?.addObserver(self, forKeyPath: key, options: options.toNSKeyValueObservingOptions, context: nil)
}

public func invalidate() {
func invalidate() {
object?.removeObserver(self, forKeyPath: key, context: nil)
object = nil
lifetimeAssociation?.cancel()
}

private var lifetimeAssociation: LifetimeAssociation?

public func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
// swiftlint:disable:next trailing_closure
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
self?.invalidate()
Expand All @@ -157,7 +157,7 @@ extension Defaults {
return self
}

public func removeLifetimeTie() {
func removeLifetimeTie() {
lifetimeAssociation?.cancel()
}

Expand Down Expand Up @@ -216,7 +216,7 @@ extension Defaults {
invalidate()
}

public func start(options: ObservationOptions) {
func start(options: ObservationOptions) {
for observable in observables {
observable.suite?.addObserver(
self,
Expand All @@ -227,7 +227,7 @@ extension Defaults {
}
}

public func invalidate() {
func invalidate() {
for observable in observables {
observable.suite?.removeObserver(self, forKeyPath: observable.key, context: &Self.observationContext)
observable.suite = nil
Expand All @@ -236,7 +236,7 @@ extension Defaults {
lifetimeAssociation?.cancel()
}

public func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
// swiftlint:disable:next trailing_closure
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
self?.invalidate()
Expand All @@ -245,7 +245,7 @@ extension Defaults {
return self
}

public func removeLifetimeTie() {
func removeLifetimeTie() {
lifetimeAssociation?.cancel()
}

Expand Down Expand Up @@ -293,6 +293,8 @@ extension Defaults {
//=> false
}
```
- Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead.
*/
public static func observe<Value: Serializable>(
_ key: Key<Value>,
Expand Down Expand Up @@ -322,6 +324,8 @@ extension Defaults {
// …
}
```
- Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead.
*/
public static func observe(
keys: _AnyKey...,
Expand Down
24 changes: 1 addition & 23 deletions Sources/Defaults/SwiftUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension Defaults {
if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) {
// The `@MainActor` is important as the `.send()` method doesn't inherit the `@MainActor` from the class.
self.task = .detached(priority: .userInitiated) { @MainActor [weak self] in
for await _ in Defaults.events(key) {
for await _ in Defaults.updates(key) {
guard let self else {
return
}
Expand Down Expand Up @@ -230,28 +230,6 @@ extension Defaults.Toggle {
}
}

extension Defaults {
// TODO: Expose this publicly at some point.
private static func events<Value: Serializable>(
_ key: Defaults.Key<Value>,
initial: Bool = true
) -> AsyncStream<Value> { // TODO: Make this `some AsyncSequence<Value>` when Swift 6 is out.
.init { continuation in
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
// TODO: Use the `.deserialize` method directly.
let value = KeyChange(change: change, defaultValue: key.defaultValue).newValue
continuation.yield(value)
}

observation.start(options: initial ? [.initial] : [])

continuation.onTermination = { _ in
observation.invalidate()
}
}
}
}

@propertyWrapper
private struct ViewStorage<Value>: DynamicProperty {
private final class ValueBox {
Expand Down
8 changes: 8 additions & 0 deletions Sources/Defaults/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,21 @@ extension Sequence {
}
}


extension Collection {
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}


extension Collection {
func indexed() -> some Sequence<(Index, Element)> {
zip(indices, self)
}
}


extension Defaults.Serializable {
/**
Cast a `Serializable` value to `Self`.
Expand Down
4 changes: 2 additions & 2 deletions Tests/DefaultsTests/DefaultsCustomBridgeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ extension PlainHourMinuteTimeRange: Defaults.Serializable {
typealias Value = PlainHourMinuteTimeRange
typealias Serializable = [PlainHourMinuteTime]

public func serialize(_ value: Value?) -> Serializable? {
func serialize(_ value: Value?) -> Serializable? {
guard let value = value else {
return nil
}

return [value.start, value.end]
}

public func deserialize(_ object: Serializable?) -> Value? {
func deserialize(_ object: Serializable?) -> Value? {
guard
let array = object,
let start = array[safe: 0],
Expand Down
12 changes: 6 additions & 6 deletions Tests/DefaultsTests/DefaultsSetAlgebraTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ struct DefaultsSetAlgebra<Element: Defaults.Serializable & Hashable>: SetAlgebra
store.contains(member)
}

func union(_ other: DefaultsSetAlgebra) -> DefaultsSetAlgebra {
DefaultsSetAlgebra(store.union(other.store))
func union(_ other: Self) -> Self {
Self(store.union(other.store))
}

func intersection(_ other: DefaultsSetAlgebra) -> DefaultsSetAlgebra {
var defaultsSetAlgebra = DefaultsSetAlgebra()
func intersection(_ other: Self) -> Self {
var defaultsSetAlgebra = Self()
defaultsSetAlgebra.store = store.intersection(other.store)
return defaultsSetAlgebra
}

func symmetricDifference(_ other: DefaultsSetAlgebra) -> DefaultsSetAlgebra {
var defaultedSetAlgebra = DefaultsSetAlgebra()
func symmetricDifference(_ other: Self) -> Self {
var defaultedSetAlgebra = Self()
defaultedSetAlgebra.store = store.symmetricDifference(other.store)
return defaultedSetAlgebra
}
Expand Down
Loading

0 comments on commit 7a22d37

Please sign in to comment.