Skip to content

Commit

Permalink
Combine support (sindresorhus#31)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
fredyshox and sindresorhus committed Jan 20, 2020
1 parent a2e2be2 commit 6029ac7
Show file tree
Hide file tree
Showing 5 changed files with 602 additions and 18 deletions.
10 changes: 10 additions & 0 deletions Defaults.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -197,6 +202,7 @@
children = (
8933C7841EB5B820000D00A4 /* Defaults.swift */,
E3EB3E34216507AE0033B089 /* Observation.swift */,
E286D0C623B8D51100570D1E /* Observation+Combine.swift */,
E3EB3E32216505920033B089 /* util.swift */,
);
path = Defaults;
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand All @@ -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 */,
Expand All @@ -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 */,
Expand Down
245 changes: 245 additions & 0 deletions Sources/Defaults/Observation+Combine.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
32 changes: 16 additions & 16 deletions Sources/Defaults/Observation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?

fileprivate init(change: [NSKeyValueChangeKey: Any]) {
final class BaseChange {
let kind: NSKeyValueChange
let indexes: IndexSet?
let isPrior: Bool
let newValue: Any?
let oldValue: Any?

init(change: [NSKeyValueChangeKey: Any]) {
kind = NSKeyValueChange(rawValue: change[.kindKey] as! UInt)!
indexes = change[.indexesKey] as? IndexSet
isPrior = change[.notificationIsPriorKey] as? Bool ?? false
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
}

Expand Down
Loading

0 comments on commit 6029ac7

Please sign in to comment.