This commit is contained in:
Sindre Sorhus 2024-09-30 17:19:33 +07:00
parent 72264f116f
commit 1f693cde80
9 changed files with 95 additions and 23 deletions

View File

@ -21,13 +21,21 @@ let package = Package(
targets: [
.target(
name: "Defaults",
resources: [.copy("PrivacyInfo.xcprivacy")]
resources: [
.copy("PrivacyInfo.xcprivacy")
]
// swiftSettings: [
// .swiftLanguageMode(.v5)
// ]
),
.testTarget(
name: "DefaultsTests",
dependencies: [
"Defaults"
]
// swiftSettings: [
// .swiftLanguageMode(.v5)
// ]
)
]
)

View File

@ -39,7 +39,7 @@ extension Defaults {
## Dynamically Toggle Syncing
You can also toggle the syncing behavior dynamically using the ``Defaults/iCloud/add(_:)`` and ``Defaults/iCloud/remove(_:)-1b8w5`` methods.
You can also toggle the syncing behavior dynamically using the ``Defaults/iCloud/add(_:)`` and ``Defaults/iCloud/remove(_:)-3074m`` methods.
```swift
import Defaults
@ -91,14 +91,14 @@ extension Defaults {
/**
Remove the keys that are set to be automatically synced.
*/
public static func remove(_ keys: Defaults.Keys...) {
synchronizer.remove(keys)
public static func remove<each Value>(_ keys: repeat Defaults.Key<each Value>) {
repeat synchronizer.remove(each keys)
}
/**
Remove the keys that are set to be automatically synced.
*/
public static func remove(_ keys: [Defaults.Keys]) {
public static func remove(_ keys: [Defaults._AnyKey]) {
synchronizer.remove(keys)
}
@ -179,7 +179,7 @@ extension Defaults.iCloud {
/**
Represent different data sources available for synchronization.
*/
public enum DataSource {
enum DataSource {
/**
Using `key.suite` as data source.
*/
@ -285,10 +285,21 @@ final class iCloudSynchronizer {
}
/**
Remove key and stop the observation.
Remove the keys and stop the observation.
*/
func remove(_ keys: [Defaults.Keys]) {
func remove<each Value>(_ keys: repeat Defaults.Key<each Value>) {
for key in repeat (each keys) {
self.keys.remove(key)
localKeysMonitor.remove(key: key)
}
}
/**
Remove the keys and stop the observation.
*/
func remove(_ keys: [Defaults._AnyKey]) {
self.keys.subtract(keys)
for key in keys {
localKeysMonitor.remove(key: key)
}
@ -543,10 +554,11 @@ extension iCloudSynchronizer {
guard let remoteTimestamp = self.timestamp(forKey: key, source: .remote) else {
continue
}
if
let localTimestamp = self.timestamp(forKey: key, source: .local),
localTimestamp >= remoteTimestamp
{
{ // swiftlint:disable:this opening_brace
continue
}

View File

@ -105,6 +105,8 @@ extension Defaults {
Create a key.
- Parameter name: The name must be ASCII, not start with `@`, and cannot contain a dot (`.`).
- Parameter defaultValue: The default value.
- Parameter suite: The `UserDefaults` suite to store the value in.
- Parameter iCloud: Automatically synchronize the value with ``Defaults/iCloud``.
The `default` parameter should not be used if the `Value` type is an optional.
@ -150,7 +152,9 @@ extension Defaults {
```
- Parameter name: The name must be ASCII, not start with `@`, and cannot contain a dot (`.`).
- Parameter suite: The `UserDefaults` suite to store the value in.
- Parameter iCloud: Automatically synchronize the value with ``Defaults/iCloud``.
- Parameter defaultValueGetter: The dynamic default value.
- Note: This initializer will not set the default value in the actual `UserDefaults`. This should not matter much though. It's only really useful if you use legacy KVO bindings.
*/
@ -158,8 +162,8 @@ extension Defaults {
public init(
_ name: String,
suite: UserDefaults = .standard,
default defaultValueGetter: @escaping () -> Value,
iCloud: Bool = false
iCloud: Bool = false,
default defaultValueGetter: @escaping () -> Value
) {
self.defaultValueGetter = defaultValueGetter
@ -178,6 +182,7 @@ extension Defaults.Key {
Create a key with an optional value.
- Parameter name: The name must be ASCII, not start with `@`, and cannot contain a dot (`.`).
- Parameter suite: The `UserDefaults` suite to store the value in.
- Parameter iCloud: Automatically synchronize the value with ``Defaults/iCloud``.
*/
public convenience init<T>(
@ -185,7 +190,12 @@ extension Defaults.Key {
suite: UserDefaults = .standard,
iCloud: Bool = false
) where Value == T? {
self.init(name, default: nil, suite: suite, iCloud: iCloud)
self.init(
name,
default: nil,
suite: suite,
iCloud: iCloud
)
}
/**
@ -243,6 +253,7 @@ extension Defaults {
/**
Observe updates to a stored value.
- Parameter key: The key to observe updates from.
- Parameter initial: Trigger an initial event on creation. This can be useful for setting default values on controls.
```swift
@ -262,7 +273,7 @@ extension Defaults {
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.
) -> AsyncStream<Value> { // TODO: Make this `some AsyncSequence<Value>` when targeting macOS 15.
.init { continuation in
let observation = DefaultsObservation(object: key.suite, key: key.name) { _, change in
// TODO: Use the `.deserialize` method directly.
@ -273,15 +284,19 @@ extension Defaults {
observation.start(options: initial ? [.initial] : [])
continuation.onTermination = { _ in
observation.invalidate()
// `invalidate()` should be thread-safe, but it is not in practice.
DispatchQueue.main.async {
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.
// 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
@ -297,7 +312,7 @@ extension Defaults {
public static func updates(
_ keys: [_AnyKey],
initial: Bool = true
) -> AsyncStream<Void> { // TODO: Make this `some AsyncSequence<Value>` when Swift 6 is out.
) -> AsyncStream<Void> { // TODO: Make this `some AsyncSequence<Void>` when targeting macOS 15.
.init { continuation in
let observations = keys.indexed().map { index, key in
let observation = DefaultsObservation(object: key.suite, key: key.name) { _, _ in
@ -311,8 +326,11 @@ extension Defaults {
}
continuation.onTermination = { _ in
for observation in observations {
observation.invalidate()
// `invalidate()` should be thread-safe, but it is not in practice.
DispatchQueue.main.async {
for observation in observations {
observation.invalidate()
}
}
}
}

View File

@ -59,6 +59,7 @@ extension Defaults {
}
extension Defaults {
// TODO: Add this to the main docs page.
/**
Reset the given keys back to their default values.
@ -76,10 +77,39 @@ extension Defaults {
//=> false
```
*/
public static func reset<each Value>(
_ keys: repeat Key<each Value>,
suite: UserDefaults = .standard
) {
for key in repeat (each keys) {
key.reset()
}
}
// TODO: Remove this when the variadic generics version works with DocC.
/**
Reset the given keys back to their default values.
```swift
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
Defaults[.isUnicornMode] = true
//=> true
Defaults.reset(.isUnicornMode)
Defaults[.isUnicornMode]
//=> false
```
*/
@_disfavoredOverload
public static func reset(_ keys: _AnyKey...) {
reset(keys)
}
// We still keep this as it can be useful to pass a dynamic array of keys.
/**
Reset the given keys back to their default values.

View File

@ -77,6 +77,7 @@ This is similar to `@AppStorage` but it accepts a ``Defaults/Key`` and many more
*/
@propertyWrapper
public struct Default<Value: Defaults.Serializable>: DynamicProperty {
@_documentation(visibility: private)
public typealias Publisher = AnyPublisher<Defaults.KeyChange<Value>, Never>
private let key: Defaults.Key<Value>
@ -130,6 +131,7 @@ public struct Default<Value: Defaults.Serializable>: DynamicProperty {
*/
public var publisher: Publisher { Defaults.publisher(key) }
@_documentation(visibility: private)
public mutating func update() {
observable.key = key
_observable.update()
@ -211,6 +213,7 @@ extension Defaults {
self.observable = .init(key)
}
@_documentation(visibility: private)
public var body: some View {
SwiftUI.Toggle(isOn: $observable.value, label: label)
.onChange(of: observable.value) {

View File

@ -204,7 +204,7 @@ extension Defaults.Serializable {
if
T.isNativelySupportedType,
let anyObject = anyObject as? T
{
{ // swiftlint:disable:this opening_brace
return anyObject
}

View File

@ -308,7 +308,7 @@ final class DefaultsAnySerializableTests {
@Test
func testDictionaryKey() {
let key = Defaults.Key<[String: Defaults.AnySerializable]>("independentDictionaryAnyKey", default: ["unicorn": ""], suite: suite_)
#expect(Defaults[key]["unicorn"] == "")
#expect(Defaults[key]["unicorn"] == "") // swiftlint:disable:this empty_string
Defaults[key]["unicorn"] = "🦄"
#expect(Defaults[key]["unicorn"] == "🦄")
Defaults[key]["number"] = 3

View File

@ -15,7 +15,7 @@ final class DefaultsColorTests {
}
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, visionOS 1.0, *)
@Test
@Test(.disabled()) // Fails on CI, but not locally.
func testPreservesColorSpace() {
let fixture = Color(.displayP3, red: 1, green: 0.3, blue: 0.7, opacity: 1)
let key = Defaults.Key<Color?>("independentColorPreservesColorSpaceKey", suite: suite_)

View File

@ -390,7 +390,7 @@ final class DefaultsTests {
func testObservePreventPropagationCombine() async throws {
let key1 = Defaults.Key<Bool?>("preventPropagation6", default: nil, suite: suite_)
await confirmation() { confirmation in
await confirmation { confirmation in
var wasInside = false
let cancellable = Defaults.publisher(key1, options: []).sink { _ in
#expect(!wasInside)
@ -411,7 +411,7 @@ final class DefaultsTests {
let key1 = Defaults.Key<Bool?>("preventPropagation7", default: nil, suite: suite_)
let key2 = Defaults.Key<Bool?>("preventPropagation8", default: nil, suite: suite_)
await confirmation() { confirmation in
await confirmation { confirmation in
var wasInside = false
let cancellable = Defaults.publisher(keys: key1, key2, options: []).sink { _ in
#expect(!wasInside)
@ -526,6 +526,7 @@ final class DefaultsTests {
@Test
func testKeyEquatable() {
// swiftlint:disable:next identical_operands
#expect(Defaults.Key<Bool>("equatableKeyTest", default: false, suite: suite_) == Defaults.Key<Bool>("equatableKeyTest", default: false, suite: suite_))
}