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 */; }; 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 */,

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
} }

View File

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