Various improvements

This commit is contained in:
Sindre Sorhus 2020-08-28 22:38:56 +02:00
parent ab8127604c
commit 110f2d2b9e
9 changed files with 119 additions and 1115 deletions

View File

@ -33,14 +33,14 @@
E38C9F28244ADA2F00A6737A /* SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38C9F26244ADA2F00A6737A /* SwiftUI.swift */; };
E38C9F29244ADA2F00A6737A /* SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38C9F26244ADA2F00A6737A /* SwiftUI.swift */; };
E38C9F2A244ADA2F00A6737A /* SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38C9F26244ADA2F00A6737A /* SwiftUI.swift */; };
E3EB3E33216505920033B089 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* util.swift */; };
E3EB3E33216505920033B089 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* Utilities.swift */; };
E3EB3E35216507AE0033B089 /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E34216507AE0033B089 /* Observation.swift */; };
E3EB3E36216507B50033B089 /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E34216507AE0033B089 /* Observation.swift */; };
E3EB3E37216507B50033B089 /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E34216507AE0033B089 /* Observation.swift */; };
E3EB3E38216507B60033B089 /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E34216507AE0033B089 /* Observation.swift */; };
E3EB3E39216507C30033B089 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* util.swift */; };
E3EB3E3A216507C40033B089 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* util.swift */; };
E3EB3E3B216507C40033B089 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* util.swift */; };
E3EB3E39216507C30033B089 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* Utilities.swift */; };
E3EB3E3A216507C40033B089 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* Utilities.swift */; };
E3EB3E3B216507C40033B089 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* Utilities.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -84,7 +84,7 @@
E339B3B22449ED2000E7A40A /* Reset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Reset.swift; sourceTree = "<group>"; usesTabs = 1; };
E339B3B72449F10D00E7A40A /* UserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = UserDefaults.swift; sourceTree = "<group>"; usesTabs = 1; };
E38C9F26244ADA2F00A6737A /* SwiftUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = SwiftUI.swift; sourceTree = "<group>"; usesTabs = 1; };
E3EB3E32216505920033B089 /* util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = util.swift; sourceTree = "<group>"; usesTabs = 1; };
E3EB3E32216505920033B089 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Utilities.swift; sourceTree = "<group>"; usesTabs = 1; };
E3EB3E34216507AE0033B089 /* Observation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Observation.swift; sourceTree = "<group>"; usesTabs = 1; };
/* End PBXFileReference section */
@ -221,7 +221,7 @@
E3EB3E34216507AE0033B089 /* Observation.swift */,
E286D0C623B8D51100570D1E /* Observation+Combine.swift */,
E38C9F26244ADA2F00A6737A /* SwiftUI.swift */,
E3EB3E32216505920033B089 /* util.swift */,
E3EB3E32216505920033B089 /* Utilities.swift */,
);
path = Defaults;
sourceTree = "<group>";
@ -512,7 +512,7 @@
8933C7851EB5B820000D00A4 /* Defaults.swift in Sources */,
E339B3B92449F10D00E7A40A /* UserDefaults.swift in Sources */,
E3EB3E35216507AE0033B089 /* Observation.swift in Sources */,
E3EB3E33216505920033B089 /* util.swift in Sources */,
E3EB3E33216505920033B089 /* Utilities.swift in Sources */,
E339B3B42449ED2000E7A40A /* Reset.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -531,7 +531,7 @@
files = (
E286D0CA23B8D54E00570D1E /* Observation+Combine.swift in Sources */,
E38C9F2A244ADA2F00A6737A /* SwiftUI.swift in Sources */,
E3EB3E3A216507C40033B089 /* util.swift in Sources */,
E3EB3E3A216507C40033B089 /* Utilities.swift in Sources */,
E339B3BB2449F10D00E7A40A /* UserDefaults.swift in Sources */,
E3EB3E37216507B50033B089 /* Observation.swift in Sources */,
8933C7871EB5B820000D00A4 /* Defaults.swift in Sources */,
@ -545,7 +545,7 @@
files = (
E286D0C923B8D54D00570D1E /* Observation+Combine.swift in Sources */,
E38C9F29244ADA2F00A6737A /* SwiftUI.swift in Sources */,
E3EB3E3B216507C40033B089 /* util.swift in Sources */,
E3EB3E3B216507C40033B089 /* Utilities.swift in Sources */,
E339B3BA2449F10D00E7A40A /* UserDefaults.swift in Sources */,
E3EB3E38216507B60033B089 /* Observation.swift in Sources */,
8933C7881EB5B820000D00A4 /* Defaults.swift in Sources */,
@ -559,7 +559,7 @@
files = (
E286D0C723B8D51100570D1E /* Observation+Combine.swift in Sources */,
E38C9F27244ADA2F00A6737A /* SwiftUI.swift in Sources */,
E3EB3E39216507C30033B089 /* util.swift in Sources */,
E3EB3E39216507C30033B089 /* Utilities.swift in Sources */,
E339B3B82449F10D00E7A40A /* UserDefaults.swift in Sources */,
E3EB3E36216507B50033B089 /* Observation.swift in Sources */,
8933C7861EB5B820000D00A4 /* Defaults.swift in Sources */,

View File

@ -1,7 +1,7 @@
// MIT License © Sindre Sorhus
import Foundation
public protocol DefaultsBaseKey: Defaults.Keys {
public protocol DefaultsBaseKey: Defaults.AnyKey {
var name: String { get }
var suite: UserDefaults { get }
}
@ -15,6 +15,7 @@ extension DefaultsBaseKey {
public enum Defaults {
public typealias BaseKey = DefaultsBaseKey
public typealias AnyKey = Keys
public class Keys: BaseKey {
public typealias Key = Defaults.Key
@ -34,7 +35,7 @@ public enum Defaults {
}
}
public final class Key<Value: Codable>: Keys {
public final class Key<Value: Codable>: AnyKey {
public let defaultValue: Value
/// Create a defaults key.
@ -58,7 +59,7 @@ public enum Defaults {
}
@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
public final class NSSecureCodingKey<Value: NSSecureCoding>: Keys {
public final class NSSecureCodingKey<Value: NSSecureCoding>: AnyKey {
public let defaultValue: Value
/// Create a defaults key.
@ -82,7 +83,7 @@ public enum Defaults {
}
@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
public final class NSSecureCodingOptionalKey<Value: NSSecureCoding>: Keys {
public final class NSSecureCodingOptionalKey<Value: NSSecureCoding>: AnyKey {
/// Create an optional defaults key.
public init(_ key: String, suite: UserDefaults = .standard) {
super.init(name: key, suite: suite)

View File

@ -1,5 +1,4 @@
#if canImport(Combine)
import Foundation
import Combine
@ -133,7 +132,7 @@ extension Defaults {
*/
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
public static func publisher(
keys: Keys...,
keys: AnyKey...,
options: ObservationOptions = [.initial]
) -> AnyPublisher<Void, Never> {
let initial = Empty<Void, Never>(completeImmediately: false).eraseToAnyPublisher()
@ -150,5 +149,4 @@ extension Defaults {
return combinedPublisher
}
}
#endif

View File

@ -141,38 +141,38 @@ extension Defaults {
}
}
private static var preventPropagationThreadDictKey: String {
private static var preventPropagationThreadDictionaryKey: String {
"\(type(of: Observation.self))_threadUpdatingValuesFlag"
}
/**
Execute block without triggering events of changes made at defaults keys.
Execute the closure without triggering change events.
Any `Defaults` key changes made within the closure will not propagate to `Defaults` event listeners (`Defaults.observe()` and `Defaults.publisher()`). This can be useful to prevent infinite recursion when you want to change a key in the callback listening to changes for the same key.
- Note: This only works with `Defaults.observe()` and `Defaults.publisher()`. User-made KVO will not be affected.
Example:
```
let observer = Defaults.observe(keys: .key1, .key2) {
//
Defaults.withoutPropagation {
// update some value at .key1
// this will not be propagated
// Update `.key1` without propagating the change to listeners.
Defaults[.key1] = 11
}
// this will be propagated
// This will be propagated.
Defaults[.someKey] = true
}
```
This only works with defaults `observe` or `publisher`. User made KVO will not be affected.
*/
public static func withoutPropagation(block: () -> Void) {
public static func withoutPropagation(_ closure: () -> Void) {
// How does it work?
// KVO observation callbacks are executed right after change is made,
// and run on the same thread as the caller. So it works by storing a flag in current
// thread's dictionary, which is then evaluated in `observeValue` callback
// KVO observation callbacks are executed right after a change is made, and run on the same thread as the caller. So it works by storing a flag in the current thread's dictionary, which is then evaluated in the callback.
let key = preventPropagationThreadDictKey
let key = preventPropagationThreadDictionaryKey
Thread.current.threadDictionary[key] = true
block()
closure()
Thread.current.threadDictionary[key] = false
}
@ -236,7 +236,7 @@ extension Defaults {
return
}
let key = preventPropagationThreadDictKey
let key = preventPropagationThreadDictionaryKey
let updatingValuesFlag = (Thread.current.threadDictionary[key] as? Bool) ?? false
guard !updatingValuesFlag else {
return
@ -260,7 +260,7 @@ extension Defaults {
}
private var observables: [SuiteKeyPair]
private var lifetimeAssociation: LifetimeAssociation? = nil
private var lifetimeAssociation: LifetimeAssociation?
private let callback: UserDefaultsKeyObservation.Callback
init(observables: [(suite: UserDefaults, key: String)], callback: @escaping UserDefaultsKeyObservation.Callback) {
@ -279,14 +279,14 @@ extension Defaults {
self,
forKeyPath: observable.key,
options: options.toNSKeyValueObservingOptions,
context: &type(of: self).observationContext
context: &Self.observationContext
)
}
}
public func invalidate() {
for observable in observables {
observable.suite?.removeObserver(self, forKeyPath: observable.key, context: &type(of: self).observationContext)
observable.suite?.removeObserver(self, forKeyPath: observable.key, context: &Self.observationContext)
observable.suite = nil
}
@ -313,7 +313,7 @@ extension Defaults {
context: UnsafeMutableRawPointer?
) {
guard
context == &type(of: self).observationContext
context == &Self.observationContext
else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
@ -326,7 +326,7 @@ extension Defaults {
return
}
let key = preventPropagationThreadDictKey
let key = preventPropagationThreadDictionaryKey
let updatingValuesFlag = (Thread.current.threadDictionary[key] as? Bool) ?? false
if updatingValuesFlag {
return
@ -401,7 +401,7 @@ extension Defaults {
}
/**
Observe multiple keys of any type, but without specific information about changes.
Observe multiple keys of any type, but without any information about the changes.
```
extension Defaults.Keys {
@ -410,12 +410,12 @@ extension Defaults {
}
let observer = Defaults.observe(keys: .setting1, .setting2) {
//...
//
}
```
*/
public static func observe(
keys: Keys...,
keys: AnyKey...,
options: ObservationOptions = [.initial],
handler: @escaping () -> Void
) -> Observation {

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
#if canImport(Combine)
import SwiftUI
import Combine
@ -111,5 +110,4 @@ public struct Default<Value: Codable>: DynamicProperty {
key.reset()
}
}
#endif

View File

@ -369,7 +369,7 @@ final class DefaultsTests: XCTestCase {
let publisher = Defaults
.publisher(key)
.compactMap { $0.newValue }
.map(\.newValue)
.eraseToAnyPublisher()
.collect(2)
@ -579,10 +579,7 @@ final class DefaultsTests: XCTestCase {
waitForExpectations(timeout: 10)
}
/**
This checks if callback is still being called, if value is changed on second thread,
while initial thread is doing some long lasting task.
*/
// This checks if the callback is still being called if the value is changed on a second thread while the initial thread is doing some long running task.
func testObservePreventPropagationMultipleThreads() {
let key1 = Defaults.Key<Int?>("preventPropagation3", default: nil)
let expect = expectation(description: "No infinite recursion")
@ -610,9 +607,7 @@ final class DefaultsTests: XCTestCase {
waitForExpectations(timeout: 10)
}
/**
Check if propagation prevention works across multiple observations
*/
// Check if propagation prevention works across multiple observations.
func testObservePreventPropagationMultipleObservations() {
let key1 = Defaults.Key<Bool?>("preventPropagation4", default: nil)
let key2 = Defaults.Key<Bool?>("preventPropagation5", default: nil)

View File

@ -239,23 +239,6 @@ Defaults[.isUnicornMode] = true
The observation will be valid until `self` is deinitialized.
### Control propagation of change events
```swift
let observer = Defaults.observe(keys: .key1, .key2) {
// …
Defaults.withoutPropagation {
// update some value at .key1
// this will not be propagated
Defaults[.key1] = 11
}
// this will be propagated
Defaults[.someKey] = true
}
```
Changes made within `Defaults.withoutPropagation` block, will not be propagated to observation callbacks, and therefore will prevent infinite recursion.
### Reset keys to their default values
```swift
@ -274,6 +257,24 @@ Defaults[.isUnicornMode]
This works for a `Key` with an optional too, which will be reset back to `nil`.
### Control propagation of change events
Changes made within the `Defaults.withoutPropagation` closure will not be propagated to observation callbacks (`Defaults.observe()` or `Defaults.publisher()`), and therefore could prevent infinite recursion.
```swift
let observer = Defaults.observe(keys: .key1, .key2) {
// …
Defaults.withoutPropagation {
// Update `.key1` without propagating the change to listeners.
Defaults[.key1] = 11
}
// This will be propagated.
Defaults[.someKey] = true
}
```
### It's just `UserDefaults` with sugar
This works too:
@ -408,9 +409,9 @@ By default, it will also trigger an initial event on creation. This can be usefu
Type: `func`
Observe changes to multiple keys of any type, but without specific information about changes.
Observe multiple keys of any type, but without any information about the changes.
Options same as in `observe` for a single key.
Options are the same as in `.observe(…)` for a single key.
#### `Defaults.publisher(_ key:, options:)`
@ -500,11 +501,11 @@ Break the lifetime tie created by `tieToLifetime(of:)`, if one exists.
The effects of any call to `tieToLifetime(of:)` are reversed. Note however that if the tied-to object has already died, then the observation is already invalid and this method has no logical effect.
#### `Defaults.withoutPropagation(_ block:)`
#### `Defaults.withoutPropagation(_ closure:)`
Execute block without emitting events of changes made at defaults keys.
Execute the closure without triggering change events.
Changes made within the block will not be propagated to observation callbacks. This only works with defaults `observe` or `publisher`. User made KVO will not be affected.
Any `Defaults` key changes made within the closure will not propagate to `Defaults` event listeners (`Defaults.observe()` and `Defaults.publisher()`). This can be useful to prevent infinite recursion when you want to change a key in the callback listening to changes for the same key.
### `@Default(_ key:)`