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
|
||||
import Foundation
|
||||
|
||||
public protocol _DefaultsBaseKey: Defaults.Keys {
|
||||
public protocol DefaultsBaseKey: Defaults.Keys {
|
||||
var name: String { get }
|
||||
var suite: UserDefaults { get }
|
||||
}
|
||||
|
||||
extension _DefaultsBaseKey {
|
||||
extension DefaultsBaseKey {
|
||||
/// Reset the item back to its default value.
|
||||
public func reset() {
|
||||
suite.removeObject(forKey: name)
|
||||
|
@ -14,7 +14,9 @@ extension _DefaultsBaseKey {
|
|||
}
|
||||
|
||||
public enum Defaults {
|
||||
public class Keys {
|
||||
public typealias BaseKey = DefaultsBaseKey
|
||||
|
||||
public class Keys: BaseKey {
|
||||
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, *)
|
||||
|
@ -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, *)
|
||||
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 let name: String
|
||||
public final class Key<Value: Codable>: Keys {
|
||||
public let defaultValue: Value
|
||||
public let suite: UserDefaults
|
||||
|
||||
/// Create a defaults key.
|
||||
/// The `default` parameter can be left out if the `Value` type is an optional.
|
||||
public init(_ key: String, default defaultValue: Value, suite: UserDefaults = .standard) {
|
||||
self.name = key
|
||||
self.defaultValue = defaultValue
|
||||
self.suite = suite
|
||||
|
||||
super.init()
|
||||
super.init(name: key, suite: suite)
|
||||
|
||||
if (defaultValue as? _DefaultsOptionalType)?.isNil == true {
|
||||
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, *)
|
||||
public final class NSSecureCodingKey<Value: NSSecureCoding>: Keys, _DefaultsBaseKey {
|
||||
public let name: String
|
||||
public final class NSSecureCodingKey<Value: NSSecureCoding>: Keys {
|
||||
public let defaultValue: Value
|
||||
public let suite: UserDefaults
|
||||
|
||||
/// Create a defaults key.
|
||||
/// The `default` parameter can be left out if the `Value` type is an optional.
|
||||
public init(_ key: String, default defaultValue: Value, suite: UserDefaults = .standard) {
|
||||
self.name = key
|
||||
self.defaultValue = defaultValue
|
||||
self.suite = suite
|
||||
|
||||
super.init()
|
||||
super.init(name: key, suite: suite)
|
||||
|
||||
if (defaultValue as? _DefaultsOptionalType)?.isNil == true {
|
||||
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, *)
|
||||
public final class NSSecureCodingOptionalKey<Value: NSSecureCoding>: Keys, _DefaultsBaseKey {
|
||||
public let name: String
|
||||
public let suite: UserDefaults
|
||||
|
||||
public final class NSSecureCodingOptionalKey<Value: NSSecureCoding>: Keys {
|
||||
/// Create an optional defaults key.
|
||||
public init(_ key: String, suite: UserDefaults = .standard) {
|
||||
self.name = key
|
||||
self.suite = suite
|
||||
super.init(name: key, 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, *)
|
||||
public static func publisher(
|
||||
keys: _DefaultsBaseKey...,
|
||||
keys: Keys...,
|
||||
options: ObservationOptions = [.initial]
|
||||
) -> AnyPublisher<Void, Never> {
|
||||
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 {
|
||||
typealias Callback = (BaseChange) -> Void
|
||||
|
||||
|
@ -201,6 +236,102 @@ extension Defaults {
|
|||
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))
|
||||
}
|
||||
}
|
||||
|
@ -268,6 +399,36 @@ extension Defaults {
|
|||
observation.start(options: options)
|
||||
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 {
|
||||
|
|
|
@ -5,7 +5,7 @@ TODO: When Swift gets support for static key paths, all of this could be simplif
|
|||
|
||||
```
|
||||
extension Defaults {
|
||||
public static func reset(_ keys: KeyPath<Keys, _DefaultsBaseKey>...) {
|
||||
public static func reset(_ keys: KeyPath<Keys, DefaultsBaseKey>...) {
|
||||
for key in keys {
|
||||
Keys[keyPath: key].reset()
|
||||
}
|
||||
|
|
|
@ -453,6 +453,53 @@ final class DefaultsTests: XCTestCase {
|
|||
waitForExpectations(timeout: 10)
|
||||
}
|
||||
|
||||
func testObserveMultipleKeys() {
|
||||
let key1 = Defaults.Key<String>("observeKey1", default: "x")
|
||||
let key2 = Defaults.Key<Bool>("observeKey2", default: true)
|
||||
let expect = expectation(description: "Observation closure being called")
|
||||
|
||||
var observation: Defaults.Observation!
|
||||
var counter = 0
|
||||
observation = Defaults.observe(keys: key1, key2, options: []) {
|
||||
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() {
|
||||
let fixtureURL = URL(string: "https://sindresorhus.com")!
|
||||
let fixtureURL2 = URL(string: "https://example.com")!
|
||||
|
@ -489,6 +536,173 @@ final class DefaultsTests: XCTestCase {
|
|||
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() {
|
||||
let defaultFixture1 = "foo1"
|
||||
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.
|
||||
|
||||
### Control propagation of change events
|
||||
|
||||
```swift
|
||||
let observer = Defaults.observe(keys: .key1, .key2) {
|
||||
// …
|
||||
Defaults.withoutPropagation {
|
||||
// update some value at .key1
|
||||
// this will not be propagated
|
||||
Defaults[.key1] = 11
|
||||
}
|
||||
// this will be propagated
|
||||
Defaults[.someKey] = true
|
||||
}
|
||||
```
|
||||
|
||||
Changes made within `Defaults.withoutPropagation` block, will not be propagated to observation callbacks, and therefore will prevent infinite recursion.
|
||||
|
||||
### Reset keys to their default values
|
||||
|
||||
```swift
|
||||
|
@ -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.
|
||||
|
||||
#### `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:)`
|
||||
|
||||
```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.
|
||||
|
||||
#### `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:)`
|
||||
|
||||
Get/set a `Defaults` item and also have the view be updated when the value changes.
|
||||
|
|
Loading…
Reference in New Issue