Various improvements
This commit is contained in:
parent
ab8127604c
commit
110f2d2b9e
|
@ -33,14 +33,14 @@
|
||||||
E38C9F28244ADA2F00A6737A /* SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38C9F26244ADA2F00A6737A /* SwiftUI.swift */; };
|
E38C9F28244ADA2F00A6737A /* SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38C9F26244ADA2F00A6737A /* SwiftUI.swift */; };
|
||||||
E38C9F29244ADA2F00A6737A /* 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 */; };
|
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 */; };
|
E3EB3E35216507AE0033B089 /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E34216507AE0033B089 /* Observation.swift */; };
|
||||||
E3EB3E36216507B50033B089 /* 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 */; };
|
E3EB3E37216507B50033B089 /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E34216507AE0033B089 /* Observation.swift */; };
|
||||||
E3EB3E38216507B60033B089 /* 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 */; };
|
E3EB3E39216507C30033B089 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* Utilities.swift */; };
|
||||||
E3EB3E3A216507C40033B089 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* util.swift */; };
|
E3EB3E3A216507C40033B089 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* Utilities.swift */; };
|
||||||
E3EB3E3B216507C40033B089 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* util.swift */; };
|
E3EB3E3B216507C40033B089 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* Utilities.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy 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; };
|
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; };
|
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; };
|
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; };
|
E3EB3E34216507AE0033B089 /* Observation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Observation.swift; sourceTree = "<group>"; usesTabs = 1; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
@ -221,7 +221,7 @@
|
||||||
E3EB3E34216507AE0033B089 /* Observation.swift */,
|
E3EB3E34216507AE0033B089 /* Observation.swift */,
|
||||||
E286D0C623B8D51100570D1E /* Observation+Combine.swift */,
|
E286D0C623B8D51100570D1E /* Observation+Combine.swift */,
|
||||||
E38C9F26244ADA2F00A6737A /* SwiftUI.swift */,
|
E38C9F26244ADA2F00A6737A /* SwiftUI.swift */,
|
||||||
E3EB3E32216505920033B089 /* util.swift */,
|
E3EB3E32216505920033B089 /* Utilities.swift */,
|
||||||
);
|
);
|
||||||
path = Defaults;
|
path = Defaults;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -512,7 +512,7 @@
|
||||||
8933C7851EB5B820000D00A4 /* Defaults.swift in Sources */,
|
8933C7851EB5B820000D00A4 /* Defaults.swift in Sources */,
|
||||||
E339B3B92449F10D00E7A40A /* UserDefaults.swift in Sources */,
|
E339B3B92449F10D00E7A40A /* UserDefaults.swift in Sources */,
|
||||||
E3EB3E35216507AE0033B089 /* Observation.swift in Sources */,
|
E3EB3E35216507AE0033B089 /* Observation.swift in Sources */,
|
||||||
E3EB3E33216505920033B089 /* util.swift in Sources */,
|
E3EB3E33216505920033B089 /* Utilities.swift in Sources */,
|
||||||
E339B3B42449ED2000E7A40A /* Reset.swift in Sources */,
|
E339B3B42449ED2000E7A40A /* Reset.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -531,7 +531,7 @@
|
||||||
files = (
|
files = (
|
||||||
E286D0CA23B8D54E00570D1E /* Observation+Combine.swift in Sources */,
|
E286D0CA23B8D54E00570D1E /* Observation+Combine.swift in Sources */,
|
||||||
E38C9F2A244ADA2F00A6737A /* SwiftUI.swift in Sources */,
|
E38C9F2A244ADA2F00A6737A /* SwiftUI.swift in Sources */,
|
||||||
E3EB3E3A216507C40033B089 /* util.swift in Sources */,
|
E3EB3E3A216507C40033B089 /* Utilities.swift in Sources */,
|
||||||
E339B3BB2449F10D00E7A40A /* UserDefaults.swift in Sources */,
|
E339B3BB2449F10D00E7A40A /* UserDefaults.swift in Sources */,
|
||||||
E3EB3E37216507B50033B089 /* Observation.swift in Sources */,
|
E3EB3E37216507B50033B089 /* Observation.swift in Sources */,
|
||||||
8933C7871EB5B820000D00A4 /* Defaults.swift in Sources */,
|
8933C7871EB5B820000D00A4 /* Defaults.swift in Sources */,
|
||||||
|
@ -545,7 +545,7 @@
|
||||||
files = (
|
files = (
|
||||||
E286D0C923B8D54D00570D1E /* Observation+Combine.swift in Sources */,
|
E286D0C923B8D54D00570D1E /* Observation+Combine.swift in Sources */,
|
||||||
E38C9F29244ADA2F00A6737A /* SwiftUI.swift in Sources */,
|
E38C9F29244ADA2F00A6737A /* SwiftUI.swift in Sources */,
|
||||||
E3EB3E3B216507C40033B089 /* util.swift in Sources */,
|
E3EB3E3B216507C40033B089 /* Utilities.swift in Sources */,
|
||||||
E339B3BA2449F10D00E7A40A /* UserDefaults.swift in Sources */,
|
E339B3BA2449F10D00E7A40A /* UserDefaults.swift in Sources */,
|
||||||
E3EB3E38216507B60033B089 /* Observation.swift in Sources */,
|
E3EB3E38216507B60033B089 /* Observation.swift in Sources */,
|
||||||
8933C7881EB5B820000D00A4 /* Defaults.swift in Sources */,
|
8933C7881EB5B820000D00A4 /* Defaults.swift in Sources */,
|
||||||
|
@ -559,7 +559,7 @@
|
||||||
files = (
|
files = (
|
||||||
E286D0C723B8D51100570D1E /* Observation+Combine.swift in Sources */,
|
E286D0C723B8D51100570D1E /* Observation+Combine.swift in Sources */,
|
||||||
E38C9F27244ADA2F00A6737A /* SwiftUI.swift in Sources */,
|
E38C9F27244ADA2F00A6737A /* SwiftUI.swift in Sources */,
|
||||||
E3EB3E39216507C30033B089 /* util.swift in Sources */,
|
E3EB3E39216507C30033B089 /* Utilities.swift in Sources */,
|
||||||
E339B3B82449F10D00E7A40A /* UserDefaults.swift in Sources */,
|
E339B3B82449F10D00E7A40A /* UserDefaults.swift in Sources */,
|
||||||
E3EB3E36216507B50033B089 /* Observation.swift in Sources */,
|
E3EB3E36216507B50033B089 /* Observation.swift in Sources */,
|
||||||
8933C7861EB5B820000D00A4 /* Defaults.swift in Sources */,
|
8933C7861EB5B820000D00A4 /* Defaults.swift in Sources */,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// MIT License © Sindre Sorhus
|
// MIT License © Sindre Sorhus
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public protocol DefaultsBaseKey: Defaults.Keys {
|
public protocol DefaultsBaseKey: Defaults.AnyKey {
|
||||||
var name: String { get }
|
var name: String { get }
|
||||||
var suite: UserDefaults { get }
|
var suite: UserDefaults { get }
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,8 @@ extension DefaultsBaseKey {
|
||||||
|
|
||||||
public enum Defaults {
|
public enum Defaults {
|
||||||
public typealias BaseKey = DefaultsBaseKey
|
public typealias BaseKey = DefaultsBaseKey
|
||||||
|
public typealias AnyKey = Keys
|
||||||
|
|
||||||
public class Keys: BaseKey {
|
public class Keys: BaseKey {
|
||||||
public typealias Key = Defaults.Key
|
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
|
public let defaultValue: Value
|
||||||
|
|
||||||
/// Create a defaults key.
|
/// 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, *)
|
@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
|
public let defaultValue: Value
|
||||||
|
|
||||||
/// Create a defaults key.
|
/// 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, *)
|
@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.
|
/// Create an optional defaults key.
|
||||||
public init(_ key: String, suite: UserDefaults = .standard) {
|
public init(_ key: String, suite: UserDefaults = .standard) {
|
||||||
super.init(name: key, suite: suite)
|
super.init(name: key, suite: suite)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#if canImport(Combine)
|
#if canImport(Combine)
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
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, *)
|
@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(
|
public static func publisher(
|
||||||
keys: Keys...,
|
keys: AnyKey...,
|
||||||
options: ObservationOptions = [.initial]
|
options: ObservationOptions = [.initial]
|
||||||
) -> AnyPublisher<Void, Never> {
|
) -> AnyPublisher<Void, Never> {
|
||||||
let initial = Empty<Void, Never>(completeImmediately: false).eraseToAnyPublisher()
|
let initial = Empty<Void, Never>(completeImmediately: false).eraseToAnyPublisher()
|
||||||
|
@ -150,5 +149,4 @@ extension Defaults {
|
||||||
return combinedPublisher
|
return combinedPublisher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -140,39 +140,39 @@ extension Defaults {
|
||||||
self.newValue = deserialize(change.newValue, to: Value.self)
|
self.newValue = deserialize(change.newValue, to: Value.self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var preventPropagationThreadDictKey: String {
|
private static var preventPropagationThreadDictionaryKey: String {
|
||||||
"\(type(of: Observation.self))_threadUpdatingValuesFlag"
|
"\(type(of: Observation.self))_threadUpdatingValuesFlag"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Execute block without triggering events of changes made at defaults keys.
|
Execute the closure without triggering change events.
|
||||||
|
|
||||||
Example:
|
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) {
|
let observer = Defaults.observe(keys: .key1, .key2) {
|
||||||
// …
|
// …
|
||||||
|
|
||||||
Defaults.withoutPropagation {
|
Defaults.withoutPropagation {
|
||||||
// update some value at .key1
|
// Update `.key1` without propagating the change to listeners.
|
||||||
// this will not be propagated
|
|
||||||
Defaults[.key1] = 11
|
Defaults[.key1] = 11
|
||||||
}
|
}
|
||||||
// this will be propagated
|
|
||||||
|
// This will be propagated.
|
||||||
Defaults[.someKey] = true
|
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?
|
// How does it work?
|
||||||
// KVO observation callbacks are executed right after change is made,
|
// 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.
|
||||||
// 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 = preventPropagationThreadDictionaryKey
|
||||||
|
|
||||||
let key = preventPropagationThreadDictKey
|
|
||||||
Thread.current.threadDictionary[key] = true
|
Thread.current.threadDictionary[key] = true
|
||||||
block()
|
closure()
|
||||||
Thread.current.threadDictionary[key] = false
|
Thread.current.threadDictionary[key] = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,8 +235,8 @@ extension Defaults {
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = preventPropagationThreadDictKey
|
let key = preventPropagationThreadDictionaryKey
|
||||||
let updatingValuesFlag = (Thread.current.threadDictionary[key] as? Bool) ?? false
|
let updatingValuesFlag = (Thread.current.threadDictionary[key] as? Bool) ?? false
|
||||||
guard !updatingValuesFlag else {
|
guard !updatingValuesFlag else {
|
||||||
return
|
return
|
||||||
|
@ -245,66 +245,66 @@ extension Defaults {
|
||||||
callback(BaseChange(change: change))
|
callback(BaseChange(change: change))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class CompositeUserDefaultsKeyObservation: NSObject, Observation {
|
private final class CompositeUserDefaultsKeyObservation: NSObject, Observation {
|
||||||
private static var observationContext = 0
|
private static var observationContext = 0
|
||||||
|
|
||||||
private final class SuiteKeyPair {
|
private final class SuiteKeyPair {
|
||||||
weak var suite: UserDefaults?
|
weak var suite: UserDefaults?
|
||||||
let key: String
|
let key: String
|
||||||
|
|
||||||
init(suite: UserDefaults, key: String) {
|
init(suite: UserDefaults, key: String) {
|
||||||
self.suite = suite
|
self.suite = suite
|
||||||
self.key = key
|
self.key = key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var observables: [SuiteKeyPair]
|
private var observables: [SuiteKeyPair]
|
||||||
private var lifetimeAssociation: LifetimeAssociation? = nil
|
private var lifetimeAssociation: LifetimeAssociation?
|
||||||
private let callback: UserDefaultsKeyObservation.Callback
|
private let callback: UserDefaultsKeyObservation.Callback
|
||||||
|
|
||||||
init(observables: [(suite: UserDefaults, key: String)], callback: @escaping UserDefaultsKeyObservation.Callback) {
|
init(observables: [(suite: UserDefaults, key: String)], callback: @escaping UserDefaultsKeyObservation.Callback) {
|
||||||
self.observables = observables.map { SuiteKeyPair(suite: $0.suite, key: $0.key) }
|
self.observables = observables.map { SuiteKeyPair(suite: $0.suite, key: $0.key) }
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func start(options: ObservationOptions) {
|
public func start(options: ObservationOptions) {
|
||||||
for observable in observables {
|
for observable in observables {
|
||||||
observable.suite?.addObserver(
|
observable.suite?.addObserver(
|
||||||
self,
|
self,
|
||||||
forKeyPath: observable.key,
|
forKeyPath: observable.key,
|
||||||
options: options.toNSKeyValueObservingOptions,
|
options: options.toNSKeyValueObservingOptions,
|
||||||
context: &type(of: self).observationContext
|
context: &Self.observationContext
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func invalidate() {
|
public func invalidate() {
|
||||||
for observable in observables {
|
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
|
observable.suite = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
lifetimeAssociation?.cancel()
|
lifetimeAssociation?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
|
public func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
|
||||||
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
|
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
|
||||||
self?.invalidate()
|
self?.invalidate()
|
||||||
})
|
})
|
||||||
|
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
public func removeLifetimeTie() {
|
public func removeLifetimeTie() {
|
||||||
lifetimeAssociation?.cancel()
|
lifetimeAssociation?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable:next block_based_kvo
|
// swiftlint:disable:next block_based_kvo
|
||||||
override func observeValue(
|
override func observeValue(
|
||||||
forKeyPath keyPath: String?,
|
forKeyPath keyPath: String?,
|
||||||
|
@ -313,29 +313,29 @@ extension Defaults {
|
||||||
context: UnsafeMutableRawPointer?
|
context: UnsafeMutableRawPointer?
|
||||||
) {
|
) {
|
||||||
guard
|
guard
|
||||||
context == &type(of: self).observationContext
|
context == &Self.observationContext
|
||||||
else {
|
else {
|
||||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard
|
guard
|
||||||
object is UserDefaults,
|
object is UserDefaults,
|
||||||
let change = change
|
let change = change
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = preventPropagationThreadDictKey
|
let key = preventPropagationThreadDictionaryKey
|
||||||
let updatingValuesFlag = (Thread.current.threadDictionary[key] as? Bool) ?? false
|
let updatingValuesFlag = (Thread.current.threadDictionary[key] as? Bool) ?? false
|
||||||
if updatingValuesFlag {
|
if updatingValuesFlag {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(BaseChange(change: change))
|
callback(BaseChange(change: change))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Observe a defaults key.
|
Observe a defaults key.
|
||||||
|
|
||||||
|
@ -399,10 +399,10 @@ extension Defaults {
|
||||||
observation.start(options: options)
|
observation.start(options: options)
|
||||||
return observation
|
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 {
|
extension Defaults.Keys {
|
||||||
static let setting1 = Key<Bool>("setting1", default: false)
|
static let setting1 = Key<Bool>("setting1", default: false)
|
||||||
|
@ -410,12 +410,12 @@ extension Defaults {
|
||||||
}
|
}
|
||||||
|
|
||||||
let observer = Defaults.observe(keys: .setting1, .setting2) {
|
let observer = Defaults.observe(keys: .setting1, .setting2) {
|
||||||
//...
|
// …
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
*/
|
*/
|
||||||
public static func observe(
|
public static func observe(
|
||||||
keys: Keys...,
|
keys: AnyKey...,
|
||||||
options: ObservationOptions = [.initial],
|
options: ObservationOptions = [.initial],
|
||||||
handler: @escaping () -> Void
|
handler: @escaping () -> Void
|
||||||
) -> Observation {
|
) -> Observation {
|
||||||
|
@ -426,7 +426,7 @@ extension Defaults {
|
||||||
handler()
|
handler()
|
||||||
}
|
}
|
||||||
compositeObservation.start(options: options)
|
compositeObservation.start(options: options)
|
||||||
|
|
||||||
return compositeObservation
|
return compositeObservation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,4 @@
|
||||||
#if canImport(Combine)
|
#if canImport(Combine)
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
|
@ -111,5 +110,4 @@ public struct Default<Value: Codable>: DynamicProperty {
|
||||||
key.reset()
|
key.reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -369,7 +369,7 @@ final class DefaultsTests: XCTestCase {
|
||||||
|
|
||||||
let publisher = Defaults
|
let publisher = Defaults
|
||||||
.publisher(key)
|
.publisher(key)
|
||||||
.compactMap { $0.newValue }
|
.map(\.newValue)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
.collect(2)
|
.collect(2)
|
||||||
|
|
||||||
|
@ -452,12 +452,12 @@ final class DefaultsTests: XCTestCase {
|
||||||
|
|
||||||
waitForExpectations(timeout: 10)
|
waitForExpectations(timeout: 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testObserveMultipleKeys() {
|
func testObserveMultipleKeys() {
|
||||||
let key1 = Defaults.Key<String>("observeKey1", default: "x")
|
let key1 = Defaults.Key<String>("observeKey1", default: "x")
|
||||||
let key2 = Defaults.Key<Bool>("observeKey2", default: true)
|
let key2 = Defaults.Key<Bool>("observeKey2", default: true)
|
||||||
let expect = expectation(description: "Observation closure being called")
|
let expect = expectation(description: "Observation closure being called")
|
||||||
|
|
||||||
var observation: Defaults.Observation!
|
var observation: Defaults.Observation!
|
||||||
var counter = 0
|
var counter = 0
|
||||||
observation = Defaults.observe(keys: key1, key2, options: []) {
|
observation = Defaults.observe(keys: key1, key2, options: []) {
|
||||||
|
@ -468,20 +468,20 @@ final class DefaultsTests: XCTestCase {
|
||||||
XCTFail()
|
XCTFail()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Defaults[key1] = "y"
|
Defaults[key1] = "y"
|
||||||
Defaults[key2] = false
|
Defaults[key2] = false
|
||||||
observation.invalidate()
|
observation.invalidate()
|
||||||
|
|
||||||
waitForExpectations(timeout: 10)
|
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, *)
|
@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() {
|
func testObserveMultipleNSSecureKeys() {
|
||||||
let key1 = Defaults.NSSecureCodingKey<ExamplePersistentHistory>("observeNSSecureCodingKey1", default: ExamplePersistentHistory(value: "TestValue"))
|
let key1 = Defaults.NSSecureCodingKey<ExamplePersistentHistory>("observeNSSecureCodingKey1", default: ExamplePersistentHistory(value: "TestValue"))
|
||||||
let key2 = Defaults.NSSecureCodingKey<ExamplePersistentHistory>("observeNSSecureCodingKey2", default: ExamplePersistentHistory(value: "TestValue"))
|
let key2 = Defaults.NSSecureCodingKey<ExamplePersistentHistory>("observeNSSecureCodingKey2", default: ExamplePersistentHistory(value: "TestValue"))
|
||||||
let expect = expectation(description: "Observation closure being called")
|
let expect = expectation(description: "Observation closure being called")
|
||||||
|
|
||||||
var observation: Defaults.Observation!
|
var observation: Defaults.Observation!
|
||||||
var counter = 0
|
var counter = 0
|
||||||
observation = Defaults.observe(keys: key1, key2, options: []) {
|
observation = Defaults.observe(keys: key1, key2, options: []) {
|
||||||
|
@ -492,7 +492,7 @@ final class DefaultsTests: XCTestCase {
|
||||||
XCTFail()
|
XCTFail()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Defaults[key1] = ExamplePersistentHistory(value: "NewTestValue1")
|
Defaults[key1] = ExamplePersistentHistory(value: "NewTestValue1")
|
||||||
Defaults[key2] = ExamplePersistentHistory(value: "NewTestValue2")
|
Defaults[key2] = ExamplePersistentHistory(value: "NewTestValue2")
|
||||||
observation.invalidate()
|
observation.invalidate()
|
||||||
|
@ -535,7 +535,7 @@ final class DefaultsTests: XCTestCase {
|
||||||
|
|
||||||
waitForExpectations(timeout: 10)
|
waitForExpectations(timeout: 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testObservePreventPropagation() {
|
func testObservePreventPropagation() {
|
||||||
let key1 = Defaults.Key<Bool?>("preventPropagation0", default: nil)
|
let key1 = Defaults.Key<Bool?>("preventPropagation0", default: nil)
|
||||||
let expect = expectation(description: "No infinite recursion")
|
let expect = expectation(description: "No infinite recursion")
|
||||||
|
@ -556,7 +556,7 @@ final class DefaultsTests: XCTestCase {
|
||||||
|
|
||||||
waitForExpectations(timeout: 10)
|
waitForExpectations(timeout: 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testObservePreventPropagationMultipleKeys() {
|
func testObservePreventPropagationMultipleKeys() {
|
||||||
let key1 = Defaults.Key<Bool?>("preventPropagation1", default: nil)
|
let key1 = Defaults.Key<Bool?>("preventPropagation1", default: nil)
|
||||||
let key2 = Defaults.Key<Bool?>("preventPropagation2", default: nil)
|
let key2 = Defaults.Key<Bool?>("preventPropagation2", default: nil)
|
||||||
|
@ -578,15 +578,12 @@ final class DefaultsTests: XCTestCase {
|
||||||
|
|
||||||
waitForExpectations(timeout: 10)
|
waitForExpectations(timeout: 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 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.
|
||||||
This checks if callback is still being called, if value is changed on second thread,
|
|
||||||
while initial thread is doing some long lasting task.
|
|
||||||
*/
|
|
||||||
func testObservePreventPropagationMultipleThreads() {
|
func testObservePreventPropagationMultipleThreads() {
|
||||||
let key1 = Defaults.Key<Int?>("preventPropagation3", default: nil)
|
let key1 = Defaults.Key<Int?>("preventPropagation3", default: nil)
|
||||||
let expect = expectation(description: "No infinite recursion")
|
let expect = expectation(description: "No infinite recursion")
|
||||||
|
|
||||||
var observation: Defaults.Observation!
|
var observation: Defaults.Observation!
|
||||||
observation = Defaults.observe(key1, options: []) { _ in
|
observation = Defaults.observe(key1, options: []) { _ in
|
||||||
Defaults.withoutPropagation {
|
Defaults.withoutPropagation {
|
||||||
|
@ -609,10 +606,8 @@ final class DefaultsTests: XCTestCase {
|
||||||
|
|
||||||
waitForExpectations(timeout: 10)
|
waitForExpectations(timeout: 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Check if propagation prevention works across multiple observations.
|
||||||
Check if propagation prevention works across multiple observations
|
|
||||||
*/
|
|
||||||
func testObservePreventPropagationMultipleObservations() {
|
func testObservePreventPropagationMultipleObservations() {
|
||||||
let key1 = Defaults.Key<Bool?>("preventPropagation4", default: nil)
|
let key1 = Defaults.Key<Bool?>("preventPropagation4", default: nil)
|
||||||
let key2 = Defaults.Key<Bool?>("preventPropagation5", default: nil)
|
let key2 = Defaults.Key<Bool?>("preventPropagation5", default: nil)
|
||||||
|
@ -627,14 +622,14 @@ final class DefaultsTests: XCTestCase {
|
||||||
}
|
}
|
||||||
expect.fulfill()
|
expect.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
Defaults[key1] = false
|
Defaults[key1] = false
|
||||||
observation1.invalidate()
|
observation1.invalidate()
|
||||||
observation2.invalidate()
|
observation2.invalidate()
|
||||||
|
|
||||||
waitForExpectations(timeout: 10)
|
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, *)
|
@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() {
|
func testObservePreventPropagationCombine() {
|
||||||
let key1 = Defaults.Key<Bool?>("preventPropagation6", default: nil)
|
let key1 = Defaults.Key<Bool?>("preventPropagation6", default: nil)
|
||||||
|
@ -649,13 +644,13 @@ final class DefaultsTests: XCTestCase {
|
||||||
}
|
}
|
||||||
expect.fulfill()
|
expect.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
Defaults[key1] = false
|
Defaults[key1] = false
|
||||||
cancellable.cancel()
|
cancellable.cancel()
|
||||||
|
|
||||||
waitForExpectations(timeout: 10)
|
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, *)
|
@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() {
|
func testObservePreventPropagationMultipleKeysCombine() {
|
||||||
let key1 = Defaults.Key<Bool?>("preventPropagation7", default: nil)
|
let key1 = Defaults.Key<Bool?>("preventPropagation7", default: nil)
|
||||||
|
@ -671,18 +666,18 @@ final class DefaultsTests: XCTestCase {
|
||||||
}
|
}
|
||||||
expect.fulfill()
|
expect.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
Defaults[key2] = false
|
Defaults[key2] = false
|
||||||
cancellable.cancel()
|
cancellable.cancel()
|
||||||
|
|
||||||
waitForExpectations(timeout: 10)
|
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, *)
|
@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() {
|
func testObservePreventPropagationModifiersCombine() {
|
||||||
let key1 = Defaults.Key<Bool?>("preventPropagation9", default: nil)
|
let key1 = Defaults.Key<Bool?>("preventPropagation9", default: nil)
|
||||||
let expect = expectation(description: "No infinite recursion")
|
let expect = expectation(description: "No infinite recursion")
|
||||||
|
|
||||||
var wasInside = false
|
var wasInside = false
|
||||||
var cancellable: AnyCancellable!
|
var cancellable: AnyCancellable!
|
||||||
cancellable = Defaults.publisher(key1, options: [])
|
cancellable = Defaults.publisher(key1, options: [])
|
||||||
|
@ -697,9 +692,9 @@ final class DefaultsTests: XCTestCase {
|
||||||
expect.fulfill()
|
expect.fulfill()
|
||||||
cancellable.cancel()
|
cancellable.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
Defaults[key1] = false
|
Defaults[key1] = false
|
||||||
|
|
||||||
waitForExpectations(timeout: 10)
|
waitForExpectations(timeout: 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
45
readme.md
45
readme.md
|
@ -239,23 +239,6 @@ Defaults[.isUnicornMode] = true
|
||||||
|
|
||||||
The observation will be valid until `self` is deinitialized.
|
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
|
### Reset keys to their default values
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
|
@ -274,6 +257,24 @@ Defaults[.isUnicornMode]
|
||||||
|
|
||||||
This works for a `Key` with an optional too, which will be reset back to `nil`.
|
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
|
### It's just `UserDefaults` with sugar
|
||||||
|
|
||||||
This works too:
|
This works too:
|
||||||
|
@ -408,9 +409,9 @@ By default, it will also trigger an initial event on creation. This can be usefu
|
||||||
|
|
||||||
Type: `func`
|
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:)`
|
#### `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.
|
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:)`
|
### `@Default(_ key:)`
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue