Add ability to subscribe to multiple keys and to prevent propagation (#49)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
This commit is contained in:
parent
c9033c70cf
commit
ab8127604c
|
@ -1,12 +1,12 @@
|
||||||
// MIT License © Sindre Sorhus
|
// MIT License © Sindre Sorhus
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public protocol _DefaultsBaseKey: Defaults.Keys {
|
public protocol DefaultsBaseKey: Defaults.Keys {
|
||||||
var name: String { get }
|
var name: String { get }
|
||||||
var suite: UserDefaults { get }
|
var suite: UserDefaults { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension _DefaultsBaseKey {
|
extension DefaultsBaseKey {
|
||||||
/// Reset the item back to its default value.
|
/// Reset the item back to its default value.
|
||||||
public func reset() {
|
public func reset() {
|
||||||
suite.removeObject(forKey: name)
|
suite.removeObject(forKey: name)
|
||||||
|
@ -14,7 +14,9 @@ extension _DefaultsBaseKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Defaults {
|
public enum Defaults {
|
||||||
public class Keys {
|
public typealias BaseKey = DefaultsBaseKey
|
||||||
|
|
||||||
|
public class Keys: BaseKey {
|
||||||
public typealias Key = Defaults.Key
|
public typealias Key = Defaults.Key
|
||||||
|
|
||||||
@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, *)
|
||||||
|
@ -23,22 +25,24 @@ 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 typealias NSSecureCodingOptionalKey = Defaults.NSSecureCodingOptionalKey
|
public typealias NSSecureCodingOptionalKey = Defaults.NSSecureCodingOptionalKey
|
||||||
|
|
||||||
fileprivate init() {}
|
public let name: String
|
||||||
|
public let suite: UserDefaults
|
||||||
|
|
||||||
|
fileprivate init(name: String, suite: UserDefaults) {
|
||||||
|
self.name = name
|
||||||
|
self.suite = suite
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class Key<Value: Codable>: Keys, _DefaultsBaseKey {
|
public final class Key<Value: Codable>: Keys {
|
||||||
public let name: String
|
|
||||||
public let defaultValue: Value
|
public let defaultValue: Value
|
||||||
public let suite: UserDefaults
|
|
||||||
|
|
||||||
/// Create a defaults key.
|
/// Create a defaults key.
|
||||||
/// The `default` parameter can be left out if the `Value` type is an optional.
|
/// The `default` parameter can be left out if the `Value` type is an optional.
|
||||||
public init(_ key: String, default defaultValue: Value, suite: UserDefaults = .standard) {
|
public init(_ key: String, default defaultValue: Value, suite: UserDefaults = .standard) {
|
||||||
self.name = key
|
|
||||||
self.defaultValue = defaultValue
|
self.defaultValue = defaultValue
|
||||||
self.suite = suite
|
|
||||||
|
|
||||||
super.init()
|
super.init(name: key, suite: suite)
|
||||||
|
|
||||||
if (defaultValue as? _DefaultsOptionalType)?.isNil == true {
|
if (defaultValue as? _DefaultsOptionalType)?.isNil == true {
|
||||||
return
|
return
|
||||||
|
@ -54,19 +58,15 @@ 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, _DefaultsBaseKey {
|
public final class NSSecureCodingKey<Value: NSSecureCoding>: Keys {
|
||||||
public let name: String
|
|
||||||
public let defaultValue: Value
|
public let defaultValue: Value
|
||||||
public let suite: UserDefaults
|
|
||||||
|
|
||||||
/// Create a defaults key.
|
/// Create a defaults key.
|
||||||
/// The `default` parameter can be left out if the `Value` type is an optional.
|
/// The `default` parameter can be left out if the `Value` type is an optional.
|
||||||
public init(_ key: String, default defaultValue: Value, suite: UserDefaults = .standard) {
|
public init(_ key: String, default defaultValue: Value, suite: UserDefaults = .standard) {
|
||||||
self.name = key
|
|
||||||
self.defaultValue = defaultValue
|
self.defaultValue = defaultValue
|
||||||
self.suite = suite
|
|
||||||
|
|
||||||
super.init()
|
super.init(name: key, suite: suite)
|
||||||
|
|
||||||
if (defaultValue as? _DefaultsOptionalType)?.isNil == true {
|
if (defaultValue as? _DefaultsOptionalType)?.isNil == true {
|
||||||
return
|
return
|
||||||
|
@ -82,14 +82,10 @@ 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, _DefaultsBaseKey {
|
public final class NSSecureCodingOptionalKey<Value: NSSecureCoding>: Keys {
|
||||||
public let name: String
|
|
||||||
public let suite: UserDefaults
|
|
||||||
|
|
||||||
/// Create an optional defaults key.
|
/// Create an optional defaults key.
|
||||||
public init(_ key: String, suite: UserDefaults = .standard) {
|
public init(_ key: String, suite: UserDefaults = .standard) {
|
||||||
self.name = key
|
super.init(name: key, suite: suite)
|
||||||
self.suite = suite
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -133,7 +133,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: _DefaultsBaseKey...,
|
keys: Keys...,
|
||||||
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()
|
||||||
|
|
|
@ -141,6 +141,41 @@ extension Defaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static var preventPropagationThreadDictKey: String {
|
||||||
|
"\(type(of: Observation.self))_threadUpdatingValuesFlag"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Execute block without triggering events of changes made at defaults keys.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This only works with defaults `observe` or `publisher`. User made KVO will not be affected.
|
||||||
|
*/
|
||||||
|
public static func withoutPropagation(block: () -> 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
|
||||||
|
Thread.current.threadDictionary[key] = true
|
||||||
|
block()
|
||||||
|
Thread.current.threadDictionary[key] = false
|
||||||
|
}
|
||||||
|
|
||||||
final class UserDefaultsKeyObservation: NSObject, Observation {
|
final class UserDefaultsKeyObservation: NSObject, Observation {
|
||||||
typealias Callback = (BaseChange) -> Void
|
typealias Callback = (BaseChange) -> Void
|
||||||
|
|
||||||
|
@ -201,6 +236,102 @@ extension Defaults {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let key = preventPropagationThreadDictKey
|
||||||
|
let updatingValuesFlag = (Thread.current.threadDictionary[key] as? Bool) ?? false
|
||||||
|
guard !updatingValuesFlag else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func invalidate() {
|
||||||
|
for observable in observables {
|
||||||
|
observable.suite?.removeObserver(self, forKeyPath: observable.key, context: &type(of: 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?,
|
||||||
|
of object: Any?,
|
||||||
|
change: [NSKeyValueChangeKey: Any]?, // swiftlint:disable:this discouraged_optional_collection
|
||||||
|
context: UnsafeMutableRawPointer?
|
||||||
|
) {
|
||||||
|
guard
|
||||||
|
context == &type(of: 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 updatingValuesFlag = (Thread.current.threadDictionary[key] as? Bool) ?? false
|
||||||
|
if updatingValuesFlag {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
callback(BaseChange(change: change))
|
callback(BaseChange(change: change))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -268,6 +399,36 @@ 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.
|
||||||
|
|
||||||
|
```
|
||||||
|
extension Defaults.Keys {
|
||||||
|
static let setting1 = Key<Bool>("setting1", default: false)
|
||||||
|
static let setting2 = Key<Bool>("setting2", default: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
let observer = Defaults.observe(keys: .setting1, .setting2) {
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
public static func observe(
|
||||||
|
keys: Keys...,
|
||||||
|
options: ObservationOptions = [.initial],
|
||||||
|
handler: @escaping () -> Void
|
||||||
|
) -> Observation {
|
||||||
|
let pairs = keys.map {
|
||||||
|
(suite: $0.suite, key: $0.name)
|
||||||
|
}
|
||||||
|
let compositeObservation = CompositeUserDefaultsKeyObservation(observables: pairs) { _ in
|
||||||
|
handler()
|
||||||
|
}
|
||||||
|
compositeObservation.start(options: options)
|
||||||
|
|
||||||
|
return compositeObservation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Defaults.ObservationOptions {
|
extension Defaults.ObservationOptions {
|
||||||
|
|
|
@ -5,7 +5,7 @@ TODO: When Swift gets support for static key paths, all of this could be simplif
|
||||||
|
|
||||||
```
|
```
|
||||||
extension Defaults {
|
extension Defaults {
|
||||||
public static func reset(_ keys: KeyPath<Keys, _DefaultsBaseKey>...) {
|
public static func reset(_ keys: KeyPath<Keys, DefaultsBaseKey>...) {
|
||||||
for key in keys {
|
for key in keys {
|
||||||
Keys[keyPath: key].reset()
|
Keys[keyPath: key].reset()
|
||||||
}
|
}
|
||||||
|
|
|
@ -453,6 +453,53 @@ final class DefaultsTests: XCTestCase {
|
||||||
waitForExpectations(timeout: 10)
|
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: []) {
|
||||||
|
counter += 1
|
||||||
|
if counter == 2 {
|
||||||
|
expect.fulfill()
|
||||||
|
} else if counter > 2 {
|
||||||
|
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: []) {
|
||||||
|
counter += 1
|
||||||
|
if counter == 2 {
|
||||||
|
expect.fulfill()
|
||||||
|
} else if counter > 2 {
|
||||||
|
XCTFail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Defaults[key1] = ExamplePersistentHistory(value: "NewTestValue1")
|
||||||
|
Defaults[key2] = ExamplePersistentHistory(value: "NewTestValue2")
|
||||||
|
observation.invalidate()
|
||||||
|
|
||||||
|
waitForExpectations(timeout: 10)
|
||||||
|
}
|
||||||
|
|
||||||
func testObserveKeyURL() {
|
func testObserveKeyURL() {
|
||||||
let fixtureURL = URL(string: "https://sindresorhus.com")!
|
let fixtureURL = URL(string: "https://sindresorhus.com")!
|
||||||
let fixtureURL2 = URL(string: "https://example.com")!
|
let fixtureURL2 = URL(string: "https://example.com")!
|
||||||
|
@ -489,6 +536,173 @@ final class DefaultsTests: XCTestCase {
|
||||||
waitForExpectations(timeout: 10)
|
waitForExpectations(timeout: 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testObservePreventPropagation() {
|
||||||
|
let key1 = Defaults.Key<Bool?>("preventPropagation0", default: nil)
|
||||||
|
let expect = expectation(description: "No infinite recursion")
|
||||||
|
|
||||||
|
var observation: Defaults.Observation!
|
||||||
|
var wasInside = false
|
||||||
|
observation = Defaults.observe(key1, options: []) { _ in
|
||||||
|
XCTAssertFalse(wasInside)
|
||||||
|
wasInside = true
|
||||||
|
Defaults.withoutPropagation {
|
||||||
|
Defaults[key1] = true
|
||||||
|
}
|
||||||
|
expect.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
Defaults[key1] = false
|
||||||
|
observation.invalidate()
|
||||||
|
|
||||||
|
waitForExpectations(timeout: 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testObservePreventPropagationMultipleKeys() {
|
||||||
|
let key1 = Defaults.Key<Bool?>("preventPropagation1", default: nil)
|
||||||
|
let key2 = Defaults.Key<Bool?>("preventPropagation2", default: nil)
|
||||||
|
let expect = expectation(description: "No infinite recursion")
|
||||||
|
|
||||||
|
var observation: Defaults.Observation!
|
||||||
|
var wasInside = false
|
||||||
|
observation = Defaults.observe(keys: key1, key2, options: []) {
|
||||||
|
XCTAssertFalse(wasInside)
|
||||||
|
wasInside = true
|
||||||
|
Defaults.withoutPropagation {
|
||||||
|
Defaults[key1] = true
|
||||||
|
}
|
||||||
|
expect.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
Defaults[key1] = false
|
||||||
|
observation.invalidate()
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
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 {
|
||||||
|
Defaults[key1]! += 1
|
||||||
|
}
|
||||||
|
print("--- Main Thread: \(Thread.isMainThread)")
|
||||||
|
if !Thread.isMainThread {
|
||||||
|
XCTAssert(Defaults[key1]! == 4)
|
||||||
|
expect.fulfill()
|
||||||
|
} else {
|
||||||
|
usleep(100000)
|
||||||
|
print("--- Release: \(Thread.isMainThread)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: .now() + 0.05) {
|
||||||
|
Defaults[key1]! += 1
|
||||||
|
}
|
||||||
|
Defaults[key1] = 1
|
||||||
|
observation.invalidate()
|
||||||
|
|
||||||
|
waitForExpectations(timeout: 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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)
|
||||||
|
let expect = expectation(description: "No infinite recursion")
|
||||||
|
|
||||||
|
let observation1 = Defaults.observe(key2, options: []) { _ in
|
||||||
|
XCTFail()
|
||||||
|
}
|
||||||
|
let observation2 = Defaults.observe(keys: key1, key2, options: []) {
|
||||||
|
Defaults.withoutPropagation {
|
||||||
|
Defaults[key2] = true
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
let expect = expectation(description: "No infinite recursion")
|
||||||
|
|
||||||
|
var wasInside = false
|
||||||
|
let cancellable = Defaults.publisher(key1, options: []).sink { _ in
|
||||||
|
XCTAssertFalse(wasInside)
|
||||||
|
wasInside = true
|
||||||
|
Defaults.withoutPropagation {
|
||||||
|
Defaults[key1] = true
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
let key2 = Defaults.Key<Bool?>("preventPropagation8", default: nil)
|
||||||
|
let expect = expectation(description: "No infinite recursion")
|
||||||
|
|
||||||
|
var wasInside = false
|
||||||
|
let cancellable = Defaults.publisher(keys: key1, key2, options: []).sink { _ in
|
||||||
|
XCTAssertFalse(wasInside)
|
||||||
|
wasInside = true
|
||||||
|
Defaults.withoutPropagation {
|
||||||
|
Defaults[key1] = true
|
||||||
|
}
|
||||||
|
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: [])
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.delay(for: 0.5, scheduler: DispatchQueue.global())
|
||||||
|
.sink { _ in
|
||||||
|
XCTAssertFalse(wasInside)
|
||||||
|
wasInside = true
|
||||||
|
Defaults.withoutPropagation {
|
||||||
|
Defaults[key1] = true
|
||||||
|
}
|
||||||
|
expect.fulfill()
|
||||||
|
cancellable.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
Defaults[key1] = false
|
||||||
|
|
||||||
|
waitForExpectations(timeout: 10)
|
||||||
|
}
|
||||||
|
|
||||||
func testResetKey() {
|
func testResetKey() {
|
||||||
let defaultFixture1 = "foo1"
|
let defaultFixture1 = "foo1"
|
||||||
let defaultFixture2 = 0
|
let defaultFixture2 = 0
|
||||||
|
|
31
readme.md
31
readme.md
|
@ -239,6 +239,23 @@ 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
|
||||||
|
@ -387,6 +404,14 @@ Observe changes to a key or an optional key.
|
||||||
|
|
||||||
By default, it will also trigger an initial event on creation. This can be useful for setting default values on controls. You can override this behavior with the `options` argument.
|
By default, it will also trigger an initial event on creation. This can be useful for setting default values on controls. You can override this behavior with the `options` argument.
|
||||||
|
|
||||||
|
#### `Defaults.observe(keys: keys..., options:)`
|
||||||
|
|
||||||
|
Type: `func`
|
||||||
|
|
||||||
|
Observe changes to multiple keys of any type, but without specific information about changes.
|
||||||
|
|
||||||
|
Options same as in `observe` for a single key.
|
||||||
|
|
||||||
#### `Defaults.publisher(_ key:, options:)`
|
#### `Defaults.publisher(_ key:, options:)`
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
|
@ -475,6 +500,12 @@ 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:)`
|
||||||
|
|
||||||
|
Execute block without emitting events of changes made at defaults keys.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
### `@Default(_ key:)`
|
### `@Default(_ key:)`
|
||||||
|
|
||||||
Get/set a `Defaults` item and also have the view be updated when the value changes.
|
Get/set a `Defaults` item and also have the view be updated when the value changes.
|
||||||
|
|
Loading…
Reference in New Issue