Skip to content

Commit

Permalink
Stream of UserDefaults values (#53)
Browse files Browse the repository at this point in the history
Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com>
  • Loading branch information
atacan and tgrapperon authored Apr 24, 2023
1 parent 53fadfd commit aa4b157
Show file tree
Hide file tree
Showing 3 changed files with 539 additions and 9 deletions.
113 changes: 111 additions & 2 deletions Sources/UserDefaultsDependency/UserDefaults+Specializations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ extension UserDefaults.Dependency {
public func set(_ value: URL?, forKey key: String) {
self._set(value, key)
}

#else
/// Returns the URL value associated with the specified key.
/// - Parameter key: A key in the current user defaults store.
Expand Down Expand Up @@ -142,7 +143,7 @@ extension UserDefaults.Dependency {
/// is no value associated to `key`, or if `R` cannot be built from the associated value.
public func rawRepresentable<R: RawRepresentable>(forKey key: String) -> R?
where R.RawValue == String {
self.string(forKey: key).flatMap(R.init(rawValue:))
self.string(forKey: key).flatMap(R.init)
}

/// Sets the value of the specified default key to the specified RawRepresentable `R` value.
Expand All @@ -160,7 +161,7 @@ extension UserDefaults.Dependency {
/// is no value associated to `key`, or if `R` cannot be built from the associated value.
public func rawRepresentable<R: RawRepresentable>(forKey key: String) -> R?
where R.RawValue == Int {
self.integer(forKey: key).flatMap(R.init(rawValue:))
self.integer(forKey: key).flatMap(R.init)
}

/// Sets the value of the specified default key to the specified RawRepresentable `R` value.
Expand All @@ -173,6 +174,106 @@ extension UserDefaults.Dependency {
}
}

extension UserDefaults.Dependency {
/// An `AsyncStream` of Boolean values for a given `key` as they change. The stream produces `nil`
/// if no value exists for the given key.
/// - Parameter key: A key in the current user defaults store.
/// - Returns: An `AsyncStream` of `Bool?` values, including the initial value.
public func boolValues(forKey key: String) -> AsyncStream<Bool?> {
self._values(key, Bool.self).map { $0 as! Bool? }.eraseToStream()
}

/// An `AsyncStream` of Data values for a given `key` as they change. The stream produces `nil` if
/// no value exists for the given key.
/// - Parameter key: A key in the current user defaults store.
/// - Returns: An `AsyncStream` of `Data?` values, including the initial value.
public func dataValues(forKey key: String) -> AsyncStream<Data?> {
self._values(key, Data.self).map { $0 as! Data? }.eraseToStream()
}

/// An `AsyncStream` of Double values for a given `key` as they change. The stream produces `nil`
/// if no value exists for the given key.
/// - Parameter key: A key in the current user defaults store.
/// - Returns: An `AsyncStream` of `Double?` values, including the initial value.
public func doubleValues(forKey key: String) -> AsyncStream<Double?> {
self._values(key, Double.self).map { $0 as! Double? }.eraseToStream()
}

/// An `AsyncStream` of Integer values for a given `key` as they change. The stream produces `nil`
/// if no value exists for the given key.
/// - Parameter key: A key in the current user defaults store.
/// - Returns: An `AsyncStream` of `Int?` values, including the initial value.
public func integerValues(forKey key: String) -> AsyncStream<Int?> {
self._values(key, Int.self).map { $0 as! Int? }.eraseToStream()
}

/// An `AsyncStream` of String values for a given `key` as they change. The stream produces `nil`
/// if no value exists for the given key.
/// - Parameter key: A key in the current user defaults store.
/// - Returns: An `AsyncStream` of `String?` values, including the initial value.
public func stringValues(forKey key: String) -> AsyncStream<String?> {
self._values(key, String.self).map { $0 as! String? }.eraseToStream()
}

#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
/// An `AsyncStream` of URL values for a given `key` as they change. The stream produces `nil`
/// if no value exists for the given key.
/// - Parameter key: A key in the current user defaults store.
/// - Returns: An `AsyncStream` of `URL?` values, including the initial value.
public func urlValues(forKey key: String) -> AsyncStream<URL?> {
self._values(key, URL.self).map { $0 as! URL? }.eraseToStream()
}
#else
/// An `AsyncStream` of URL values for a given `key` as they change. The stream produces `nil`
/// if no value exists for the given key.
/// - Parameter key: A key in the current user defaults store.
/// - Returns: An `AsyncStream` of `URL?` values, including the initial value.
@available(
*, unavailable, message: "Observing URLs from UserDefaults is not supported on this platform."
)
public func urlValues(forKey key: String) -> AsyncStream<URL?> {
self._values(key, URL.self).map { $0 as! URL? }.eraseToStream()
}
#endif

/// An `AsyncStream` of `RawRepresentable<String>` values for a given `key` as they change. The
/// stream produces `nil` if no value exists for the given key.
/// - Parameters:
/// - valueType: The type of `RawRepresentable` values that is produced. You can use this
/// argument if the type system is unable ot infer it from the context.
/// - key: A key in the current user defaults store.
/// - Returns: An `AsyncStream` of `RawRepresentable<String>?` values, including the initial
/// value.
public func rawRepresentableValues<R: RawRepresentable>(
_ valueType: R.Type = R.self,
forKey key: String
)
-> AsyncStream<R?> where R.RawValue == String
{
self._values(key, String.self)
.map { ($0 as! String?).flatMap(R.init) }
.eraseToStream()
}

/// An `AsyncStream` of `RawRepresentable<Int>` values for a given `key` as they change. The
/// stream produces `nil` if no value exists for the given key.
/// - Parameters:
/// - valueType: The type of `RawRepresentable` values that is produced. You can use this
/// argument if the type system is unable ot infer it from the context.
/// - key: A key in the current user defaults store.
/// - Returns: An `AsyncStream` of `RawRepresentable<Int>?` values, including the initial value.
public func rawRepresentableValues<R: RawRepresentable>(
_ valueType: R.Type = R.self,
forKey key: String
)
-> AsyncStream<R?> where R.RawValue == Int
{
self._values(key, Int.self)
.map { ($0 as! Int?).flatMap(R.init) }
.eraseToStream()
}
}

// NS Extensions
extension UserDefaults.Dependency {
/// Returns the Date value associated with the specified key.
Expand All @@ -191,4 +292,12 @@ extension UserDefaults.Dependency {
public func set(_ value: Date?, forKey key: String) {
self._set(value, key)
}

/// An `AsyncStream` of Date values for a given `key` as they change. The stream produces `nil` if
/// if no value exists for the given key.
/// - Parameter key: A key in the current user defaults store.
/// - Returns: An `AsyncStream` of `Date?` values, including the initial value.
public func dateValues(forKey key: String) -> AsyncStream<Date?> {
self._values(key, Date.self).map { $0 as! Date? }.eraseToStream()
}
}
36 changes: 29 additions & 7 deletions Sources/UserDefaultsDependency/UserDefaultsDependency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -270,25 +270,47 @@ extension UserDefaults.Dependency: TestDependencyKey {
return UserDefaults.Dependency { key, _ in
storage.value[key]
} set: { value, key in
var valueDidChange = false
storage.withValue {
valueDidChange = !_isEqual($0[key], value)
$0[key] = value
}
for continuation in continuations.value[key]?.values ?? [:].values {
continuation.yield(value)
if valueDidChange {
for continuation in continuations.value[key]?.values ?? [:].values {
continuation.yield(value)
}
}
} values: { key, _ in
let id = UUID()
let stream = AsyncStream((any Sendable)?.self) { streamContinuation in
continuations.withValue {
$0[key, default: [:]][id] = streamContinuation
}
let (stream, continuation) = AsyncStream.streamWithContinuation(
(any Sendable)?.self,
bufferingPolicy: .bufferingNewest(1)
)
continuations.withValue {
$0[key, default: [:]][id] = continuation
}
defer { continuations.value[key]?[id]?.yield(storage.value[key]) }
continuation.yield(storage.value[key])
return stream
}
}
}

fileprivate func _isEqual(_ lhs: (any Sendable)?, _ rhs: (any Sendable)?) -> Bool {
switch (lhs, rhs) {
case let (.some(lhs), .some(rhs)):
return (lhs as! any Equatable).isEqual(other: rhs)
case (.none, .none):
return type(of: lhs) == type(of: rhs)
case (.some, .none), (.none, .some): return false
}
}

extension Equatable {
fileprivate func isEqual(other: Any) -> Bool {
self == other as? Self
}
}

#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
extension UserDefaults.Dependency {
/// An iCloud-based container of key-value pairs you use to share data among
Expand Down
Loading

0 comments on commit aa4b157

Please sign in to comment.