Refactor internal `UserDefaults` observation class (#181)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
This commit is contained in:
parent
b8c1e7c869
commit
bf717462d9
|
@ -249,11 +249,11 @@ final class iCloudSynchronizer {
|
||||||
@Atomic(value: []) private var remoteSyncingKeys: Set<Defaults.Keys>
|
@Atomic(value: []) private var remoteSyncingKeys: Set<Defaults.Keys>
|
||||||
|
|
||||||
// TODO: Replace it with async stream when Swift supports custom executors.
|
// TODO: Replace it with async stream when Swift supports custom executors.
|
||||||
private lazy var localKeysMonitor: Defaults.CompositeUserDefaultsAnyKeyObservation = .init { [weak self] observable in
|
private lazy var localKeysMonitor: Defaults.CompositeDefaultsObservation = .init { [weak self] pair, _ in
|
||||||
guard
|
guard
|
||||||
let self,
|
let self,
|
||||||
let suite = observable.suite,
|
let suite = pair.suite,
|
||||||
let key = keys.first(where: { $0.name == observable.key && $0.suite == suite }),
|
let key = keys.first(where: { $0.name == pair.key && $0.suite == suite }),
|
||||||
// Prevent triggering local observation when syncing from remote.
|
// Prevent triggering local observation when syncing from remote.
|
||||||
!remoteSyncingKeys.contains(key)
|
!remoteSyncingKeys.contains(key)
|
||||||
else {
|
else {
|
||||||
|
@ -273,7 +273,7 @@ final class iCloudSynchronizer {
|
||||||
self.keys.formUnion(keys)
|
self.keys.formUnion(keys)
|
||||||
syncWithoutWaiting(keys)
|
syncWithoutWaiting(keys)
|
||||||
for key in keys {
|
for key in keys {
|
||||||
localKeysMonitor.addObserver(key)
|
localKeysMonitor.add(key: key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,7 +283,7 @@ final class iCloudSynchronizer {
|
||||||
func remove(_ keys: [Defaults.Keys]) {
|
func remove(_ keys: [Defaults.Keys]) {
|
||||||
self.keys.subtract(keys)
|
self.keys.subtract(keys)
|
||||||
for key in keys {
|
for key in keys {
|
||||||
localKeysMonitor.removeObserver(key)
|
localKeysMonitor.remove(key: key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -239,7 +239,7 @@ extension Defaults {
|
||||||
initial: Bool = true
|
initial: Bool = true
|
||||||
) -> AsyncStream<Value> { // TODO: Make this `some AsyncSequence<Value>` when Swift 6 is out.
|
) -> AsyncStream<Value> { // TODO: Make this `some AsyncSequence<Value>` when Swift 6 is out.
|
||||||
.init { continuation in
|
.init { continuation in
|
||||||
let observation = UserDefaultsKeyObservation2(object: key.suite, key: key.name) { change in
|
let observation = DefaultsObservation(object: key.suite, key: key.name) { _, change in
|
||||||
// TODO: Use the `.deserialize` method directly.
|
// TODO: Use the `.deserialize` method directly.
|
||||||
let value = KeyChange(change: change, defaultValue: key.defaultValue).newValue
|
let value = KeyChange(change: change, defaultValue: key.defaultValue).newValue
|
||||||
continuation.yield(value)
|
continuation.yield(value)
|
||||||
|
@ -275,7 +275,7 @@ extension Defaults {
|
||||||
) -> AsyncStream<Void> { // TODO: Make this `some AsyncSequence<Value>` when Swift 6 is out.
|
) -> AsyncStream<Void> { // TODO: Make this `some AsyncSequence<Value>` when Swift 6 is out.
|
||||||
.init { continuation in
|
.init { continuation in
|
||||||
let observations = keys.indexed().map { index, key in
|
let observations = keys.indexed().map { index, key in
|
||||||
let observation = UserDefaultsKeyObservation2(object: key.suite, key: key.name) { _ in
|
let observation = DefaultsObservation(object: key.suite, key: key.name) { _, _ in
|
||||||
continuation.yield()
|
continuation.yield()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,16 +7,16 @@ extension Defaults {
|
||||||
*/
|
*/
|
||||||
final class DefaultsSubscription<SubscriberType: Subscriber>: Subscription where SubscriberType.Input == BaseChange {
|
final class DefaultsSubscription<SubscriberType: Subscriber>: Subscription where SubscriberType.Input == BaseChange {
|
||||||
private var subscriber: SubscriberType?
|
private var subscriber: SubscriberType?
|
||||||
private var observation: UserDefaultsKeyObservation?
|
private var observation: DefaultsObservationWithLifeTime?
|
||||||
private let options: ObservationOptions
|
private let options: ObservationOptions
|
||||||
|
|
||||||
init(subscriber: SubscriberType, suite: UserDefaults, key: String, options: ObservationOptions) {
|
init(subscriber: SubscriberType, suite: UserDefaults, key: String, options: ObservationOptions) {
|
||||||
self.subscriber = subscriber
|
self.subscriber = subscriber
|
||||||
self.options = options
|
self.options = options
|
||||||
self.observation = UserDefaultsKeyObservation(
|
self.observation = DefaultsObservationWithLifeTime(
|
||||||
object: suite,
|
object: suite,
|
||||||
key: key,
|
key: key,
|
||||||
callback: observationCallback(_:)
|
observationCallback
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ extension Defaults {
|
||||||
observation?.start(options: options)
|
observation?.start(options: options)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func observationCallback(_ change: BaseChange) {
|
private func observationCallback(_: SuiteKeyPair, change: BaseChange) {
|
||||||
_ = subscriber?.receive(change)
|
_ = subscriber?.receive(change)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,145 +119,6 @@ extension Defaults {
|
||||||
Thread.current.threadDictionary[key] = false
|
Thread.current.threadDictionary[key] = false
|
||||||
}
|
}
|
||||||
|
|
||||||
final class UserDefaultsKeyObservation: NSObject, Observation {
|
|
||||||
typealias Callback = (BaseChange) -> Void
|
|
||||||
|
|
||||||
private weak var object: UserDefaults?
|
|
||||||
private let key: String
|
|
||||||
private let callback: Callback
|
|
||||||
private var isObserving = false
|
|
||||||
|
|
||||||
init(object: UserDefaults, key: String, callback: @escaping Callback) {
|
|
||||||
self.object = object
|
|
||||||
self.key = key
|
|
||||||
self.callback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func start(options: ObservationOptions) {
|
|
||||||
object?.addObserver(self, forKeyPath: key, options: options.toNSKeyValueObservingOptions, context: nil)
|
|
||||||
isObserving = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func invalidate() {
|
|
||||||
if isObserving {
|
|
||||||
object?.removeObserver(self, forKeyPath: key, context: nil)
|
|
||||||
isObserving = false
|
|
||||||
}
|
|
||||||
|
|
||||||
object = nil
|
|
||||||
lifetimeAssociation?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var lifetimeAssociation: LifetimeAssociation?
|
|
||||||
|
|
||||||
func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
|
|
||||||
// swiftlint:disable:next trailing_closure
|
|
||||||
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
|
|
||||||
self?.invalidate()
|
|
||||||
})
|
|
||||||
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
|
|
||||||
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 let selfObject = self.object else {
|
|
||||||
invalidate()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard
|
|
||||||
selfObject == (object as? NSObject),
|
|
||||||
let change
|
|
||||||
else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = preventPropagationThreadDictionaryKey
|
|
||||||
let updatingValuesFlag = (Thread.current.threadDictionary[key] as? Bool) ?? false
|
|
||||||
guard !updatingValuesFlag else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(BaseChange(change: change))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same as the above, but without the lifetime utilities, which slows down invalidation and we don't need them for `.updates()`.
|
|
||||||
final class UserDefaultsKeyObservation2: NSObject {
|
|
||||||
typealias Callback = (BaseChange) -> Void
|
|
||||||
|
|
||||||
private weak var object: UserDefaults?
|
|
||||||
private let key: String
|
|
||||||
private let callback: Callback
|
|
||||||
private var isObserving = false
|
|
||||||
|
|
||||||
init(object: UserDefaults, key: String, callback: @escaping Callback) {
|
|
||||||
self.object = object
|
|
||||||
self.key = key
|
|
||||||
self.callback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func start(options: ObservationOptions) {
|
|
||||||
object?.addObserver(self, forKeyPath: key, options: options.toNSKeyValueObservingOptions, context: nil)
|
|
||||||
isObserving = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func invalidate() {
|
|
||||||
if isObserving {
|
|
||||||
object?.removeObserver(self, forKeyPath: key, context: nil)
|
|
||||||
isObserving = false
|
|
||||||
}
|
|
||||||
|
|
||||||
object = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 let selfObject = self.object else {
|
|
||||||
invalidate()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard
|
|
||||||
selfObject == (object as? NSObject),
|
|
||||||
let change
|
|
||||||
else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = preventPropagationThreadDictionaryKey
|
|
||||||
let updatingValuesFlag = (Thread.current.threadDictionary[key] as? Bool) ?? false
|
|
||||||
guard !updatingValuesFlag else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(BaseChange(change: change))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class SuiteKeyPair: Hashable {
|
final class SuiteKeyPair: Hashable {
|
||||||
weak var suite: UserDefaults?
|
weak var suite: UserDefaults?
|
||||||
let key: String
|
let key: String
|
||||||
|
@ -278,17 +139,30 @@ extension Defaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class CompositeUserDefaultsKeyObservation: NSObject, Observation {
|
/**
|
||||||
private static var observationContext = 0
|
Standard observation for `Defaults`.
|
||||||
|
The only class which handle the low level observation.
|
||||||
|
*/
|
||||||
|
final class DefaultsObservation: NSObject {
|
||||||
|
typealias Callback = (SuiteKeyPair, BaseChange) -> Void
|
||||||
|
|
||||||
private var observables: [SuiteKeyPair]
|
static var observationContext = 0
|
||||||
private var lifetimeAssociation: LifetimeAssociation?
|
private weak var suite: UserDefaults?
|
||||||
private let callback: UserDefaultsKeyObservation.Callback
|
private let name: String
|
||||||
|
private let callback: Callback
|
||||||
|
private var isObserving = false
|
||||||
|
private let lock: Lock = .make()
|
||||||
|
|
||||||
init(observables: [(suite: UserDefaults, key: String)], callback: @escaping UserDefaultsKeyObservation.Callback) {
|
init(object: UserDefaults, key: String, _ callback: @escaping Callback) {
|
||||||
self.observables = observables.map { SuiteKeyPair(suite: $0.suite, key: $0.key) }
|
self.suite = object
|
||||||
|
self.name = key
|
||||||
|
self.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
init(key: Defaults._AnyKey, _ callback: @escaping Callback) {
|
||||||
|
self.suite = key.suite
|
||||||
|
self.name = key.name
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
super.init()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
@ -296,36 +170,24 @@ extension Defaults {
|
||||||
}
|
}
|
||||||
|
|
||||||
func start(options: ObservationOptions) {
|
func start(options: ObservationOptions) {
|
||||||
for observable in observables {
|
lock.with {
|
||||||
observable.suite?.addObserver(
|
guard !isObserving else {
|
||||||
self,
|
return
|
||||||
forKeyPath: observable.key,
|
}
|
||||||
options: options.toNSKeyValueObservingOptions,
|
suite?.addObserver(self, forKeyPath: name, options: options.toNSKeyValueObservingOptions, context: &Self.observationContext)
|
||||||
context: &Self.observationContext
|
isObserving = true
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func invalidate() {
|
func invalidate() {
|
||||||
for observable in observables {
|
lock.with {
|
||||||
observable.suite?.removeObserver(self, forKeyPath: observable.key, context: &Self.observationContext)
|
guard isObserving else {
|
||||||
observable.suite = nil
|
return
|
||||||
}
|
}
|
||||||
|
suite?.removeObserver(self, forKeyPath: name)
|
||||||
lifetimeAssociation?.cancel()
|
isObserving = false
|
||||||
|
suite = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
|
|
||||||
// swiftlint:disable:next trailing_closure
|
|
||||||
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
|
|
||||||
self?.invalidate()
|
|
||||||
})
|
|
||||||
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeLifetimeTie() {
|
|
||||||
lifetimeAssociation?.cancel()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable:next block_based_kvo
|
// swiftlint:disable:next block_based_kvo
|
||||||
|
@ -342,8 +204,13 @@ extension Defaults {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard let selfSuite = suite else {
|
||||||
|
invalidate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
guard
|
guard
|
||||||
object is UserDefaults,
|
selfSuite == (object as? UserDefaults),
|
||||||
let change
|
let change
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
|
@ -355,39 +222,36 @@ extension Defaults {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(BaseChange(change: change))
|
callback(SuiteKeyPair(suite: selfSuite, key: name), BaseChange(change: change))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class CompositeUserDefaultsAnyKeyObservation: NSObject, Observation {
|
/**
|
||||||
typealias Callback = (SuiteKeyPair) -> Void
|
Observation that wraps `DefaultsObservation` and adds a lifetime association.
|
||||||
private static var observationContext = 1
|
*/
|
||||||
|
final class DefaultsObservationWithLifeTime: Observation {
|
||||||
private var observables: Set<SuiteKeyPair> = []
|
private var observation: DefaultsObservation
|
||||||
private var lifetimeAssociation: LifetimeAssociation?
|
private var lifetimeAssociation: LifetimeAssociation?
|
||||||
private let callback: CompositeUserDefaultsAnyKeyObservation.Callback
|
|
||||||
|
|
||||||
init(_ callback: @escaping CompositeUserDefaultsAnyKeyObservation.Callback) {
|
init(object: UserDefaults, key: String, _ callback: @escaping DefaultsObservation.Callback) {
|
||||||
self.callback = callback
|
self.observation = .init(object: object, key: key, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addObserver(_ key: Defaults._AnyKey, options: ObservationOptions = []) {
|
init(key: Defaults._AnyKey, _ callback: @escaping DefaultsObservation.Callback) {
|
||||||
let keyPair: SuiteKeyPair = .init(suite: key.suite, key: key.name)
|
self.observation = .init(key: key, callback)
|
||||||
let (inserted, observable) = observables.insert(keyPair)
|
|
||||||
guard inserted else {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
observable.suite?.addObserver(self, forKeyPath: observable.key, options: options.toNSKeyValueObservingOptions, context: &Self.observationContext)
|
deinit {
|
||||||
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeObserver(_ key: Defaults._AnyKey) {
|
func start(options: ObservationOptions) {
|
||||||
let keyPair: SuiteKeyPair = .init(suite: key.suite, key: key.name)
|
observation.start(options: options)
|
||||||
guard let observable = observables.remove(keyPair) else {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
observable.suite?.removeObserver(self, forKeyPath: observable.key, context: &Self.observationContext)
|
func invalidate() {
|
||||||
|
observation.invalidate()
|
||||||
|
lifetimeAssociation?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
|
@ -403,40 +267,63 @@ extension Defaults {
|
||||||
func removeLifetimeTie() {
|
func removeLifetimeTie() {
|
||||||
lifetimeAssociation?.cancel()
|
lifetimeAssociation?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func invalidate() {
|
|
||||||
for observable in observables {
|
|
||||||
observable.suite?.removeObserver(self, forKeyPath: observable.key, context: &Self.observationContext)
|
|
||||||
observable.suite = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
observables.removeAll()
|
/**
|
||||||
|
Observation that manages multiple `DefaultsObservation`.
|
||||||
|
Can add or remove the observed key dynamically.
|
||||||
|
*/
|
||||||
|
final class CompositeDefaultsObservation: Observation {
|
||||||
|
private var observations: Set<DefaultsObservation> = []
|
||||||
|
private let callback: DefaultsObservation.Callback
|
||||||
|
private var lifetimeAssociation: LifetimeAssociation?
|
||||||
|
|
||||||
|
init(_ callback: @escaping DefaultsObservation.Callback) {
|
||||||
|
self.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func start(options: ObservationOptions) {
|
||||||
|
observations.forEach { $0.start(options: options) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidate() {
|
||||||
|
observations.forEach { $0.invalidate() }
|
||||||
lifetimeAssociation?.cancel()
|
lifetimeAssociation?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable:next block_based_kvo
|
func add(key: Defaults._AnyKey, options: ObservationOptions = []) {
|
||||||
override func observeValue(
|
let (isInserted, observation) = observations.insert(DefaultsObservation(key: key, callback))
|
||||||
forKeyPath keyPath: String?,
|
guard isInserted else {
|
||||||
of object: Any?,
|
|
||||||
change: [NSKeyValueChangeKey: Any]?, // swiftlint:disable:this discouraged_optional_collection
|
|
||||||
context: UnsafeMutableRawPointer?
|
|
||||||
) {
|
|
||||||
guard
|
|
||||||
context == &Self.observationContext
|
|
||||||
else {
|
|
||||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard
|
observation.start(options: options)
|
||||||
let object = object as? UserDefaults,
|
}
|
||||||
let keyPath,
|
|
||||||
let observable = observables.first(where: { $0.key == keyPath && $0.suite == object })
|
func remove(key: Defaults._AnyKey) {
|
||||||
else {
|
guard let observation = observations.remove(DefaultsObservation(key: key, self.callback)) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(observable)
|
observation.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
|
||||||
|
// swiftlint:disable:next trailing_closure
|
||||||
|
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
|
||||||
|
self?.invalidate()
|
||||||
|
})
|
||||||
|
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeLifetimeTie() {
|
||||||
|
lifetimeAssociation?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,7 +348,7 @@ extension Defaults {
|
||||||
options: ObservationOptions = [.initial],
|
options: ObservationOptions = [.initial],
|
||||||
handler: @escaping (KeyChange<Value>) -> Void
|
handler: @escaping (KeyChange<Value>) -> Void
|
||||||
) -> some Observation {
|
) -> some Observation {
|
||||||
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
|
let observation = DefaultsObservationWithLifeTime(key: key) { _, change in
|
||||||
handler(
|
handler(
|
||||||
KeyChange(change: change, defaultValue: key.defaultValue)
|
KeyChange(change: change, defaultValue: key.defaultValue)
|
||||||
)
|
)
|
||||||
|
@ -491,12 +378,14 @@ extension Defaults {
|
||||||
options: ObservationOptions = [.initial],
|
options: ObservationOptions = [.initial],
|
||||||
handler: @escaping () -> Void
|
handler: @escaping () -> Void
|
||||||
) -> some Observation {
|
) -> some Observation {
|
||||||
let pairs = keys.map {
|
let compositeObservation = CompositeDefaultsObservation { _, _ in
|
||||||
(suite: $0.suite, key: $0.name)
|
|
||||||
}
|
|
||||||
let compositeObservation = CompositeUserDefaultsKeyObservation(observables: pairs) { _ in
|
|
||||||
handler()
|
handler()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for key in keys {
|
||||||
|
compositeObservation.add(key: key)
|
||||||
|
}
|
||||||
|
|
||||||
compositeObservation.start(options: options)
|
compositeObservation.start(options: options)
|
||||||
|
|
||||||
return compositeObservation
|
return compositeObservation
|
||||||
|
|
|
@ -29,6 +29,7 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
final class DefaultsSwiftUITests: XCTestCase {
|
final class DefaultsSwiftUITests: XCTestCase {
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
|
|
Loading…
Reference in New Issue