Combine support (#31)

Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
This commit is contained in:
Kacper Rączy 2020-01-20 17:41:13 +01:00 committed by Sindre Sorhus
parent a2e2be2d5d
commit 6029ac796b
5 changed files with 601 additions and 17 deletions

View File

@ -17,6 +17,10 @@
8933C7901EB5B82D000D00A4 /* DefaultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8933C7891EB5B82A000D00A4 /* DefaultsTests.swift */; };
DD7502881C68FEDE006590AF /* Defaults.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6DA0F1BF000BD002C0205 /* Defaults.framework */; };
DD7502921C690C7A006590AF /* Defaults.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D9F01BEFFFBE002C0205 /* Defaults.framework */; };
E286D0C723B8D51100570D1E /* Observation+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = E286D0C623B8D51100570D1E /* Observation+Combine.swift */; };
E286D0C823B8D54C00570D1E /* Observation+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = E286D0C623B8D51100570D1E /* Observation+Combine.swift */; };
E286D0C923B8D54D00570D1E /* Observation+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = E286D0C623B8D51100570D1E /* Observation+Combine.swift */; };
E286D0CA23B8D54E00570D1E /* Observation+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = E286D0C623B8D51100570D1E /* Observation+Combine.swift */; };
E3EB3E33216505920033B089 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* util.swift */; };
E3EB3E35216507AE0033B089 /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E34216507AE0033B089 /* Observation.swift */; };
E3EB3E36216507B50033B089 /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E34216507AE0033B089 /* Observation.swift */; };
@ -64,6 +68,7 @@
AD2FAA281CD0B6E100659CF4 /* DefaultsTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = DefaultsTests.plist; sourceTree = "<group>"; };
DD75027A1C68FCFC006590AF /* Defaults-macOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Defaults-macOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
DD75028D1C690C7A006590AF /* Defaults-tvOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Defaults-tvOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
E286D0C623B8D51100570D1E /* Observation+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Observation+Combine.swift"; sourceTree = "<group>"; };
E3EB3E32216505920033B089 /* util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = util.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 */
@ -197,6 +202,7 @@
children = (
8933C7841EB5B820000D00A4 /* Defaults.swift */,
E3EB3E34216507AE0033B089 /* Observation.swift */,
E286D0C623B8D51100570D1E /* Observation+Combine.swift */,
E3EB3E32216505920033B089 /* util.swift */,
);
path = Defaults;
@ -483,6 +489,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E286D0C823B8D54C00570D1E /* Observation+Combine.swift in Sources */,
8933C7851EB5B820000D00A4 /* Defaults.swift in Sources */,
E3EB3E35216507AE0033B089 /* Observation.swift in Sources */,
E3EB3E33216505920033B089 /* util.swift in Sources */,
@ -501,6 +508,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E286D0CA23B8D54E00570D1E /* Observation+Combine.swift in Sources */,
E3EB3E3A216507C40033B089 /* util.swift in Sources */,
E3EB3E37216507B50033B089 /* Observation.swift in Sources */,
8933C7871EB5B820000D00A4 /* Defaults.swift in Sources */,
@ -511,6 +519,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E286D0C923B8D54D00570D1E /* Observation+Combine.swift in Sources */,
E3EB3E3B216507C40033B089 /* util.swift in Sources */,
E3EB3E38216507B60033B089 /* Observation.swift in Sources */,
8933C7881EB5B820000D00A4 /* Defaults.swift in Sources */,
@ -521,6 +530,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E286D0C723B8D51100570D1E /* Observation+Combine.swift in Sources */,
E3EB3E39216507C30033B089 /* util.swift in Sources */,
E3EB3E36216507B50033B089 /* Observation.swift in Sources */,
8933C7861EB5B820000D00A4 /* Defaults.swift in Sources */,

View File

@ -0,0 +1,245 @@
import Foundation
import Combine
extension Defaults {
/**
Custom `Subscription` for `UserDefaults` key observation.
*/
@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, *)
final class DefaultsSubscription<SubscriberType: Subscriber>: Subscription where SubscriberType.Input == BaseChange {
private var subscriber: SubscriberType?
private var observation: UserDefaultsKeyObservation?
init(subscriber: SubscriberType, suite: UserDefaults, key: String, options: NSKeyValueObservingOptions) {
self.subscriber = subscriber
self.observation = UserDefaultsKeyObservation(
object: suite,
key: key,
callback: observationCallback(_:)
)
self.observation?.start(options: options)
}
func request(_ demand: Subscribers.Demand) {
// Nothing as we send events only when they occur.
}
func cancel() {
observation?.invalidate()
observation = nil
subscriber = nil
}
private func observationCallback(_ change: BaseChange) {
_ = subscriber?.receive(change)
}
}
/**
Custom Publisher, which is using DefaultsSubscription.
*/
@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, *)
struct DefaultsPublisher: Publisher {
typealias Output = BaseChange
typealias Failure = Never
private let suite: UserDefaults
private let key: String
private let options: NSKeyValueObservingOptions
init(suite: UserDefaults, key: String, options: NSKeyValueObservingOptions) {
self.suite = suite
self.key = key
self.options = options
}
func receive<S>(subscriber: S) where S : Subscriber, DefaultsPublisher.Failure == S.Failure, DefaultsPublisher.Output == S.Input {
let subscription = DefaultsSubscription(
subscriber: subscriber,
suite: suite,
key: key,
options: options
)
subscriber.receive(subscription: subscription)
}
}
/**
Returns a type-erased `Publisher` that publishes changes related to the given key.
```
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
let publisher = Defaults.publisher(.isUnicornMode).map { $0.newValue }
let cancellable = publisher.sink { value in
print(value)
//=> false
}
```
*/
@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<T: Codable>(
_ key: Defaults.Key<T>,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<KeyChange<T>, Never> {
let publisher = DefaultsPublisher(suite: key.suite, key: key.name, options: options)
.map { KeyChange<T>(change: $0, defaultValue: key.defaultValue) }
return AnyPublisher(publisher)
}
/**
Returns a type-erased `Publisher` that publishes changes related to the given key.
*/
@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<T: NSSecureCoding>(
_ key: Defaults.NSSecureCodingKey<T>,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<NSSecureCodingKeyChange<T>, Never> {
let publisher = DefaultsPublisher(suite: key.suite, key: key.name, options: options)
.map { NSSecureCodingKeyChange<T>(change: $0, defaultValue: key.defaultValue) }
return AnyPublisher(publisher)
}
/**
Returns a type-erased `Publisher` that publishes changes related to the given optional key.
*/
@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<T: Codable>(
_ key: Defaults.OptionalKey<T>,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<OptionalKeyChange<T>, Never> {
let publisher = DefaultsPublisher(suite: key.suite, key: key.name, options: options)
.map { OptionalKeyChange<T>(change: $0) }
return AnyPublisher(publisher)
}
/**
Returns a type-erased `Publisher` that publishes changes related to the given optional key.
*/
@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<T: NSSecureCoding>(
_ key: Defaults.NSSecureCodingOptionalKey<T>,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<NSSecureCodingOptionalKeyChange<T>, Never> {
let publisher = DefaultsPublisher(suite: key.suite, key: key.name, options: options)
.map { NSSecureCodingOptionalKeyChange<T>(change: $0) }
return AnyPublisher(publisher)
}
/**
Publisher for multiple `Key<T>` observation, but without specific information about changes.
*/
@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<T: Codable>(
keys: Defaults.Key<T>...,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<Void, Never> {
let initial = Empty<Void, Never>(completeImmediately: false).eraseToAnyPublisher()
let combinedPublisher =
keys.map { key in
return Defaults.publisher(key, options: options)
.map { _ in () }
.eraseToAnyPublisher()
}.reduce(initial) { (combined, keyPublisher) in
combined.merge(with: keyPublisher).eraseToAnyPublisher()
}
return combinedPublisher
}
/**
Publisher for multiple `OptionalKey<T>` observation, but without specific information about changes.
*/
@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<T: Codable>(
keys: Defaults.OptionalKey<T>...,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<Void, Never> {
let initial = Empty<Void, Never>(completeImmediately: false).eraseToAnyPublisher()
let combinedPublisher =
keys.map { key in
return Defaults.publisher(key, options: options)
.map { _ in () }
.eraseToAnyPublisher()
}.reduce(initial) { (combined, keyPublisher) in
combined.merge(with: keyPublisher).eraseToAnyPublisher()
}
return combinedPublisher
}
/**
Publisher for multiple `NSSecureCodingKey<T>` observation, but without specific information about changes.
*/
@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<T: NSSecureCoding>(
keys: Defaults.NSSecureCodingKey<T>...,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<Void, Never> {
let initial = Empty<Void, Never>(completeImmediately: false).eraseToAnyPublisher()
let combinedPublisher =
keys.map { key in
return Defaults.publisher(key, options: options)
.map { _ in () }
.eraseToAnyPublisher()
}.reduce(initial) { (combined, keyPublisher) in
combined.merge(with: keyPublisher).eraseToAnyPublisher()
}
return combinedPublisher
}
/**
Publisher for multiple `NSSecureCodingOptionalKey<T>` observation, but without specific information about changes.
*/
@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<T: NSSecureCoding>(
keys: Defaults.NSSecureCodingOptionalKey<T>...,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<Void, Never> {
let initial = Empty<Void, Never>(completeImmediately: false).eraseToAnyPublisher()
let combinedPublisher =
keys.map { key in
return Defaults.publisher(key, options: options)
.map { _ in () }
.eraseToAnyPublisher()
}.reduce(initial) { (combined, keyPublisher) in
combined.merge(with: keyPublisher).eraseToAnyPublisher()
}
return combinedPublisher
}
/**
Convenience `Publisher` for all `UserDefaults` key change events. A wrapper around the `UserDefaults.didChangeNotification`.
- Parameter initialEvent: Trigger an initial event immediately.
*/
@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 publisherAll(initialEvent: Bool = true) -> AnyPublisher<Void, Never> {
let publisher =
NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)
.map { _ in () }
if initialEvent {
return publisher
.prepend(())
.eraseToAnyPublisher()
} else {
return publisher
.eraseToAnyPublisher()
}
}
}

View File

@ -64,14 +64,14 @@ extension Defaults {
return try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(dataValue) as? T
}
fileprivate final class BaseChange {
fileprivate let kind: NSKeyValueChange
fileprivate let indexes: IndexSet?
fileprivate let isPrior: Bool
fileprivate let newValue: Any?
fileprivate let oldValue: Any?
final class BaseChange {
let kind: NSKeyValueChange
let indexes: IndexSet?
let isPrior: Bool
let newValue: Any?
let oldValue: Any?
fileprivate init(change: [NSKeyValueChangeKey: Any]) {
init(change: [NSKeyValueChangeKey: Any]) {
kind = NSKeyValueChange(rawValue: change[.kindKey] as! UInt)!
indexes = change[.indexesKey] as? IndexSet
isPrior = change[.notificationIsPriorKey] as? Bool ?? false
@ -87,7 +87,7 @@ extension Defaults {
public let newValue: T
public let oldValue: T
fileprivate init(change: BaseChange, defaultValue: T) {
init(change: BaseChange, defaultValue: T) {
self.kind = change.kind
self.indexes = change.indexes
self.isPrior = change.isPrior
@ -104,7 +104,7 @@ extension Defaults {
public let newValue: T
public let oldValue: T
fileprivate init(change: BaseChange, defaultValue: T) {
init(change: BaseChange, defaultValue: T) {
self.kind = change.kind
self.indexes = change.indexes
self.isPrior = change.isPrior
@ -120,7 +120,7 @@ extension Defaults {
public let newValue: T?
public let oldValue: T?
fileprivate init(change: BaseChange) {
init(change: BaseChange) {
self.kind = change.kind
self.indexes = change.indexes
self.isPrior = change.isPrior
@ -137,7 +137,7 @@ extension Defaults {
public let newValue: T?
public let oldValue: T?
fileprivate init(change: BaseChange) {
init(change: BaseChange) {
self.kind = change.kind
self.indexes = change.indexes
self.isPrior = change.isPrior
@ -146,14 +146,14 @@ extension Defaults {
}
}
private final class UserDefaultsKeyObservation: NSObject, DefaultsObservation {
fileprivate typealias Callback = (BaseChange) -> Void
final class UserDefaultsKeyObservation: NSObject, DefaultsObservation {
typealias Callback = (BaseChange) -> Void
private weak var object: UserDefaults?
private let key: String
private let callback: Callback
fileprivate init(object: UserDefaults, key: String, callback: @escaping Callback) {
init(object: UserDefaults, key: String, callback: @escaping Callback) {
self.object = object
self.key = key
self.callback = callback
@ -163,7 +163,7 @@ extension Defaults {
invalidate()
}
fileprivate func start(options: NSKeyValueObservingOptions) {
func start(options: NSKeyValueObservingOptions) {
object?.addObserver(self, forKeyPath: key, options: options, context: nil)
}

View File

@ -2,6 +2,7 @@ import Foundation
import XCTest
import Defaults
import CoreData
import Combine
let fixtureURL = URL(string: "https://sindresorhus.com")!
let fixtureURL2 = URL(string: "https://example.com")!
@ -165,6 +166,224 @@ final class DefaultsTests: XCTestCase {
Defaults.removeAll(suite: customSuite)
}
@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 testObserveKeyCombine() {
let key = Defaults.Key<Bool>("observeKey", default: false)
let expect = expectation(description: "Observation closure being called")
let publisher = Defaults
.publisher(key, options: [.old, .new])
.map { ($0.oldValue, $0.newValue) }
.collect(2)
let cancellable = publisher.sink { tuples in
for (i, expected) in [(false, true), (true, false)].enumerated() {
XCTAssertEqual(expected.0, tuples[i].0)
XCTAssertEqual(expected.1, tuples[i].1)
}
expect.fulfill()
}
Defaults[key] = true
Defaults.reset(key)
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 testObserveNSSecureCodingKeyCombine() {
let key = Defaults.NSSecureCodingKey<ExamplePersistentHistory>("observeNSSecureCodingKey", default: ExamplePersistentHistory(value: "TestValue"))
let expect = expectation(description: "Observation closure being called")
let publisher = Defaults
.publisher(key, options: [.old, .new])
.map { ($0.oldValue.value, $0.newValue.value) }
.collect(3)
let expectedValues = [
("TestValue", "NewTestValue"),
("NewTestValue", "NewTestValue2"),
("NewTestValue2", "TestValue")
]
let cancellable = publisher.sink { actualValues in
for (expected, actual) in zip(expectedValues, actualValues) {
XCTAssertEqual(expected.0, actual.0)
XCTAssertEqual(expected.1, actual.1)
}
expect.fulfill()
}
Defaults[key] = ExamplePersistentHistory(value: "NewTestValue")
Defaults[key] = ExamplePersistentHistory(value: "NewTestValue2")
Defaults.reset(key)
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 testObserveOptionalKeyCombine() {
let key = Defaults.OptionalKey<Bool>("observeOptionalKey")
let expect = expectation(description: "Observation closure being called")
let publisher = Defaults
.publisher(key, options: [.old, .new])
.map { ($0.oldValue, $0.newValue) }
.collect(3)
let expectedValues: [(Bool?, Bool?)] = [(nil, true), (true, false), (false, nil)]
let cancellable = publisher.sink { actualValues in
for (expected, actual) in zip(expectedValues, actualValues) {
XCTAssertEqual(expected.0, actual.0)
XCTAssertEqual(expected.1, actual.1)
}
expect.fulfill()
}
Defaults[key] = true
Defaults[key] = false
Defaults.reset(key)
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 testObserveNSSecureCodingOptionalKeyCombine() {
let key = Defaults.NSSecureCodingOptionalKey<ExamplePersistentHistory>("observeNSSecureCodingOptionalKey")
let expect = expectation(description: "Observation closure being called")
let publisher = Defaults
.publisher(key, options: [.old, .new])
.map { ($0.oldValue?.value, $0.newValue?.value) }
.collect(3)
let expectedValues: [(String?, String?)] = [
(nil, "NewTestValue"),
("NewTestValue", "NewTestValue2"),
("NewTestValue2", nil)
]
let cancellable = publisher.sink { actualValues in
for (expected, actual) in zip(expectedValues, actualValues) {
XCTAssertEqual(expected.0, actual.0)
XCTAssertEqual(expected.1, actual.1)
}
expect.fulfill()
}
XCTAssertNil(Defaults[key])
Defaults[key] = ExamplePersistentHistory(value: "NewTestValue")
Defaults[key] = ExamplePersistentHistory(value: "NewTestValue2")
Defaults.reset(key)
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 testObserveAllCombine() {
let key = Defaults.Key<Bool>("observeAllKey", default: false)
let expect = expectation(description: "Observation closure being called")
let publisher = Defaults.publisherAll().collect(3)
let cancellable = publisher.sink { actualValues in
XCTAssertEqual(3, actualValues.count)
expect.fulfill()
}
Defaults[key] = true
Defaults[key] = 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 testObserveMultipleKeysCombine() {
let key1 = Defaults.Key<Bool>("observeKey1", default: false)
let key2 = Defaults.Key<Bool>("observeKey2", default: true)
let expect = expectation(description: "Observation closure being called")
let publisher = Defaults.publisher(keys: key1, key2, options: [.old, .new]).collect(2)
let cancellable = publisher.sink { _ in
expect.fulfill()
}
Defaults[key1] = true
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 testObserveMultipleNSSecureKeysCombine() {
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")
let publisher = Defaults.publisher(keys: key1, key2, options: [.old, .new]).collect(2)
let cancellable = publisher.sink { _ in
expect.fulfill()
}
Defaults[key1] = ExamplePersistentHistory(value: "NewTestValue1")
Defaults[key2] = ExamplePersistentHistory(value: "NewTestValue2")
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 testObserveMultipleOptionalKeysCombine() {
let key1 = Defaults.OptionalKey<Bool>("observeOptionalKey1")
let key2 = Defaults.OptionalKey<Bool>("observeOptionalKey2")
let expect = expectation(description: "Observation closure being called")
let publisher = Defaults.publisher(keys: key1, key2, options: [.old, .new]).collect(2)
let cancellable = publisher.sink { _ in
expect.fulfill()
}
Defaults[key1] = true
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 testObserveMultipleNSSecureOptionalKeysCombine() {
let key1 = Defaults.NSSecureCodingOptionalKey<ExamplePersistentHistory>("observeNSSecureCodingKey1")
let key2 = Defaults.NSSecureCodingOptionalKey<ExamplePersistentHistory>("observeNSSecureCodingKey2")
let expect = expectation(description: "Observation closure being called")
let publisher = Defaults.publisher(keys: key1, key2, options: [.old, .new]).collect(2)
let cancellable = publisher.sink { _ in
expect.fulfill()
}
Defaults[key1] = ExamplePersistentHistory(value: "NewTestValue1")
Defaults[key2] = ExamplePersistentHistory(value: "NewTestValue2")
cancellable.cancel()
waitForExpectations(timeout: 10)
}
func testObserveKey() {
let key = Defaults.Key<Bool>("observeKey", default: false)
let expect = expectation(description: "Observation closure being called")

114
readme.md
View File

@ -13,6 +13,7 @@ It's used in production by apps like [Gifski](https://github.com/sindresorhus/Gi
- **NSSecureCoding support:** You can store any [NSSecureCoding](https://developer.apple.com/documentation/foundation/nssecurecoding) value.
- **Debuggable:** The data is stored as JSON-serialized values.
- **Observation:** Observe changes to keys.
- **Publishers:** Combine publishers built-in.
- **Lightweight:** It's only some hundred lines of code.
## Compatibility
@ -163,6 +164,31 @@ Defaults[.isUnicornMode] = true
In contrast to the native `UserDefaults` key observation, here you receive a strongly-typed change object.
There is also an observation API using the [Combine](https://developer.apple.com/documentation/combine) framework, exposing a [Publisher](https://developer.apple.com/documentation/combine/publisher) for key changes:
```swift
let publisher = Defaults.publisher(.isUnicornMode)
let cancellable = publisher.sink { change in
// Initial event
print(change.oldValue)
//=> false
print(change.newValue)
//=> false
// First actual event
print(change.oldValue)
//=> false
print(change.newValue)
//=> true
}
Defaults[.isUnicornMode] = true
// To invalidate the observation.
cancellable.cancel()
```
### Invalidate observations automatically
```swift
@ -329,7 +355,7 @@ Defaults.observe<T: Codable>(
```
```swift
Defaults.observe<T: Codable>(
Defaults.observe<T: NSSecureCoding>(
_ key: Defaults.NSSecureCodingKey<T>,
options: NSKeyValueObservingOptions = [.initial, .old, .new],
handler: @escaping (NSSecureCodingKeyChange<T>) -> Void
@ -345,7 +371,7 @@ Defaults.observe<T: Codable>(
```
```swift
Defaults.observe<T: Codable>(
Defaults.observe<T: NSSecureCoding>(
_ key: Defaults.NSSecureCodingOptionalKey<T>,
options: NSKeyValueObservingOptions = [.initial, .old, .new],
handler: @escaping (NSSecureCodingOptionalKeyChange<T>) -> Void
@ -358,6 +384,90 @@ 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.publisher`
```swift
Defaults.publisher<T: Codable>(
_ key: Defaults.Key<T>,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<KeyChange<T>, Never>
```
```swift
Defaults.publisher<T: NSSecureCoding>(
_ key: Defaults.NSSecureCodingKey<T>,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<NSSecureCodingKeyChange<T>, Never>
```
```swift
Defaults.publisher<T: Codable>(
_ key: Defaults.OptionalKey<T>,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<OptionalKeyChange<T>, Never>
```
```swift
Defaults.publisher<T: NSSecureCoding>(
_ key: Defaults.NSSecureCodingOptionalKey<T>,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<NSSecureCodingOptionalKeyChange<T>, Never>
```
Type: `func`
Observation API using [Publisher](https://developer.apple.com/documentation/combine/publisher) from the [Combine](https://developer.apple.com/documentation/combine) framework.
Available on macOS 10.15+, iOS 13.0+, tvOS 13.0+, and watchOS 6.0+.
#### `Defaults.publisher(keys:)`
```swift
Defaults.publisher<T: Codable>(
keys: Defaults.Key<T>...,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<Void, Never> {
```
```swift
Defaults.publisher<T: NSSecureCoding>(
keys: Defaults.NSSecureCodingKey<T>...,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<Void, Never> {
```
```swift
Defaults.publisher<T: Codable>(
keys: Defaults.OptionalKey<T>...,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<Void, Never> {
```
```swift
Defaults.publisher<T: NSSecureCoding>(
keys: Defaults.NSSecureCodingOptionalKey<T>...,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<Void, Never> {
```
Type: `func`
[Combine](https://developer.apple.com/documentation/combine) observation API for multiple key observation, but without specific information about changes.
Available on macOS 10.15+, iOS 13.0+, tvOS 13.0+, and watchOS 6.0+.
#### `Defaults.publisherAll`
```swift
Defaults.publisherAll(initialEvent: Bool = true) -> AnyPublisher<UserDefaults, Never>
```
Convenience [Publisher](https://developer.apple.com/documentation/combine/publisher) for all `UserDefaults` key change events. A wrapper around the [`UserDefaults.didChangeNotification` notification](https://developer.apple.com/documentation/foundation/userdefaults/1408206-didchangenotification).
- Parameter `initialEvent`: Trigger an initial event immediately.
Available on macOS 10.15+, iOS 13.0+, tvOS 13.0+, and watchOS 6.0+.
#### `Defaults.removeAll`
```swift