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,7 +15,8 @@ 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

@ -140,39 +140,39 @@ extension Defaults {
self.newValue = deserialize(change.newValue, to: Value.self)
}
}
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.
Example:
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.
```
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
let key = preventPropagationThreadDictKey
// 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 = preventPropagationThreadDictionaryKey
Thread.current.threadDictionary[key] = true
block()
closure()
Thread.current.threadDictionary[key] = false
}
@ -235,8 +235,8 @@ extension Defaults {
else {
return
}
let key = preventPropagationThreadDictKey
let key = preventPropagationThreadDictionaryKey
let updatingValuesFlag = (Thread.current.threadDictionary[key] as? Bool) ?? false
guard !updatingValuesFlag else {
return
@ -245,66 +245,66 @@ extension Defaults {
callback(BaseChange(change: change))
}
}
private final class CompositeUserDefaultsKeyObservation: NSObject, Observation {
private static var observationContext = 0
private final class SuiteKeyPair {
weak var suite: UserDefaults?
let key: String
init(suite: UserDefaults, key: String) {
self.suite = suite
self.key = key
}
}
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) {
self.observables = observables.map { SuiteKeyPair(suite: $0.suite, key: $0.key) }
self.callback = callback
super.init()
}
deinit {
invalidate()
}
public func start(options: ObservationOptions) {
for observable in observables {
observable.suite?.addObserver(
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
}
lifetimeAssociation?.cancel()
}
public func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
self?.invalidate()
})
return self
}
public func removeLifetimeTie() {
lifetimeAssociation?.cancel()
}
// swiftlint:disable:next block_based_kvo
override func observeValue(
forKeyPath keyPath: String?,
@ -313,29 +313,29 @@ 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
}
guard
object is UserDefaults,
let change = change
else {
return
}
let key = preventPropagationThreadDictKey
let key = preventPropagationThreadDictionaryKey
let updatingValuesFlag = (Thread.current.threadDictionary[key] as? Bool) ?? false
if updatingValuesFlag {
return
}
callback(BaseChange(change: change))
}
}
/**
Observe a defaults key.
@ -399,10 +399,10 @@ extension Defaults {
observation.start(options: options)
return observation
}
/**
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 {
static let setting1 = Key<Bool>("setting1", default: false)
@ -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 {
@ -426,7 +426,7 @@ extension Defaults {
handler()
}
compositeObservation.start(options: options)
return compositeObservation
}
}

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)
@ -452,12 +452,12 @@ final class DefaultsTests: XCTestCase {
waitForExpectations(timeout: 10)
}
func testObserveMultipleKeys() {
let key1 = Defaults.Key<String>("observeKey1", default: "x")
let key2 = Defaults.Key<Bool>("observeKey2", default: true)
let expect = expectation(description: "Observation closure being called")
var observation: Defaults.Observation!
var counter = 0
observation = Defaults.observe(keys: key1, key2, options: []) {
@ -468,20 +468,20 @@ final class DefaultsTests: XCTestCase {
XCTFail()
}
}
Defaults[key1] = "y"
Defaults[key2] = false
observation.invalidate()
waitForExpectations(timeout: 10)
}
@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, *)
func testObserveMultipleNSSecureKeys() {
let key1 = Defaults.NSSecureCodingKey<ExamplePersistentHistory>("observeNSSecureCodingKey1", default: ExamplePersistentHistory(value: "TestValue"))
let key2 = Defaults.NSSecureCodingKey<ExamplePersistentHistory>("observeNSSecureCodingKey2", default: ExamplePersistentHistory(value: "TestValue"))
let expect = expectation(description: "Observation closure being called")
var observation: Defaults.Observation!
var counter = 0
observation = Defaults.observe(keys: key1, key2, options: []) {
@ -492,7 +492,7 @@ final class DefaultsTests: XCTestCase {
XCTFail()
}
}
Defaults[key1] = ExamplePersistentHistory(value: "NewTestValue1")
Defaults[key2] = ExamplePersistentHistory(value: "NewTestValue2")
observation.invalidate()
@ -535,7 +535,7 @@ final class DefaultsTests: XCTestCase {
waitForExpectations(timeout: 10)
}
func testObservePreventPropagation() {
let key1 = Defaults.Key<Bool?>("preventPropagation0", default: nil)
let expect = expectation(description: "No infinite recursion")
@ -556,7 +556,7 @@ final class DefaultsTests: XCTestCase {
waitForExpectations(timeout: 10)
}
func testObservePreventPropagationMultipleKeys() {
let key1 = Defaults.Key<Bool?>("preventPropagation1", default: nil)
let key2 = Defaults.Key<Bool?>("preventPropagation2", default: nil)
@ -578,15 +578,12 @@ 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")
var observation: Defaults.Observation!
observation = Defaults.observe(key1, options: []) { _ in
Defaults.withoutPropagation {
@ -609,10 +606,8 @@ 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)
@ -627,14 +622,14 @@ final class DefaultsTests: XCTestCase {
}
expect.fulfill()
}
Defaults[key1] = false
observation1.invalidate()
observation2.invalidate()
waitForExpectations(timeout: 10)
}
@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, *)
func testObservePreventPropagationCombine() {
let key1 = Defaults.Key<Bool?>("preventPropagation6", default: nil)
@ -649,13 +644,13 @@ final class DefaultsTests: XCTestCase {
}
expect.fulfill()
}
Defaults[key1] = false
cancellable.cancel()
waitForExpectations(timeout: 10)
}
@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, *)
func testObservePreventPropagationMultipleKeysCombine() {
let key1 = Defaults.Key<Bool?>("preventPropagation7", default: nil)
@ -671,18 +666,18 @@ final class DefaultsTests: XCTestCase {
}
expect.fulfill()
}
Defaults[key2] = false
cancellable.cancel()
waitForExpectations(timeout: 10)
}
@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, *)
func testObservePreventPropagationModifiersCombine() {
let key1 = Defaults.Key<Bool?>("preventPropagation9", default: nil)
let expect = expectation(description: "No infinite recursion")
var wasInside = false
var cancellable: AnyCancellable!
cancellable = Defaults.publisher(key1, options: [])
@ -697,9 +692,9 @@ final class DefaultsTests: XCTestCase {
expect.fulfill()
cancellable.cancel()
}
Defaults[key1] = false
waitForExpectations(timeout: 10)
}

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:)`