Get rid of `OptionalKey`

This commit is contained in:
Sindre Sorhus 2020-01-21 14:29:57 +07:00
parent 3275717838
commit b2fdee2055
9 changed files with 86 additions and 230 deletions

View File

@ -1,3 +1,3 @@
language: swift
osx_image: xcode11.3
osx_image: xcode11.4
script: xcodebuild test -project Defaults.xcodeproj -scheme Defaults-macOS

View File

@ -8,7 +8,7 @@ Pod::Spec.new do |s|
s.authors = { 'Sindre Sorhus' => 'sindresorhus@gmail.com' }
s.source = { :git => 'https://github.com/sindresorhus/Defaults.git', :tag => "v#{s.version}" }
s.source_files = 'Sources/**/*.swift'
s.swift_version = '5.1'
s.swift_version = '5.2'
s.macos.deployment_target = '10.12'
s.ios.deployment_target = '10.0'
s.tvos.deployment_target = '10.0'

View File

@ -1,4 +1,4 @@
// swift-tools-version:5.1
// swift-tools-version:5.2
import PackageDescription
let package = Package(

View File

@ -8,8 +8,6 @@ public final class 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 NSSecureCodingKey = Defaults.NSSecureCodingKey
public typealias OptionalKey = Defaults.OptionalKey
@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
@ -29,6 +27,10 @@ public final class Defaults {
super.init()
if (defaultValue as? _DefaultsOptionalType)?.isNil == true {
return
}
// Sets the default value in the actual UserDefaults, so it can be used in other contexts, like binding.
if UserDefaults.isNativelySupportedType(Value.self) {
suite.register(defaults: [key: defaultValue])
@ -52,6 +54,10 @@ public final class Defaults {
super.init()
if (defaultValue as? _DefaultsOptionalType)?.isNil == true {
return
}
// Sets the default value in the actual UserDefaults, so it can be used in other contexts, like binding.
if UserDefaults.isNativelySupportedType(Value.self) {
suite.register(defaults: [key: defaultValue])
@ -61,17 +67,6 @@ public final class Defaults {
}
}
public final class OptionalKey<Value: Codable>: Keys {
public let name: String
public let suite: UserDefaults
/// Create an optional defaults key.
public init(_ key: String, suite: UserDefaults = .standard) {
self.name = key
self.suite = suite
}
}
@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 {
public let name: String
@ -103,15 +98,7 @@ public final class Defaults {
}
}
/// Access a defaults value using a `Defaults.OptionalKey`.
public static subscript<Value: Codable>(key: OptionalKey<Value>) -> Value? {
get { key.suite[key] }
set {
key.suite[key] = newValue
}
}
/// Access a defaults value using a `Defaults.OptionalKey`.
/// Access a defaults value using a `Defaults.NSSecureCodingOptionalKey`.
@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 static subscript<Value: NSSecureCoding>(key: NSSecureCodingOptionalKey<Value>) -> Value? {
get { key.suite[key] }
@ -193,29 +180,6 @@ public final class Defaults {
key.suite[key] = key.defaultValue
}
}
/**
Reset the given optional keys back to `nil`.
- Parameter keys: Keys to reset.
- Parameter suite: `UserDefaults` suite.
```
extension Defaults.Keys {
static let unicorn = OptionalKey<String>("unicorn")
}
Defaults[.unicorn] = "🦄"
Defaults.reset(.unicorn)
Defaults[.unicorn]
//=> nil
```
*/
public static func reset<Value: Codable>(_ keys: OptionalKey<Value>..., suite: UserDefaults = .standard) {
reset(keys, suite: suite)
}
/**
Reset the given optional keys back to `nil`.
@ -228,31 +192,6 @@ public final class Defaults {
public static func reset<Value: NSSecureCoding>(_ keys: NSSecureCodingOptionalKey<Value>..., suite: UserDefaults = .standard) {
reset(keys, suite: suite)
}
/**
Reset the given array of optional keys back to `nil`.
- Parameter keys: Keys to reset.
- Parameter suite: `UserDefaults` suite.
```
extension Defaults.Keys {
static let unicorn = OptionalKey<String>("unicorn")
}
Defaults[.unicorn] = "🦄"
Defaults.reset(.unicorn)
Defaults[.unicorn]
//=> nil
```
*/
public static func reset<Value: Codable>(_ keys: [OptionalKey<Value>], suite: UserDefaults = .standard) {
for key in keys {
key.suite[key] = nil
}
}
/**
Reset the given array of optional keys back to `nil`.
@ -277,6 +216,19 @@ public final class Defaults {
}
}
extension Defaults.Key where Value: _DefaultsOptionalType {
public convenience init(_ key: String, suite: UserDefaults = .standard) {
self.init(key, default: nil, suite: suite)
}
}
@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, *)
extension Defaults.NSSecureCodingKey where Value: _DefaultsOptionalType {
public convenience init(_ key: String, suite: UserDefaults = .standard) {
self.init(key, default: nil, suite: suite)
}
}
extension UserDefaults {
private func _get<Value: Codable>(_ key: String) -> Value? {
if UserDefaults.isNativelySupportedType(Value.self) {
@ -334,6 +286,11 @@ extension UserDefaults {
}
private func _set<Value: Codable>(_ key: String, to value: Value) {
if (value as? _DefaultsOptionalType)?.isNil == true {
removeObject(forKey: key)
return
}
if UserDefaults.isNativelySupportedType(Value.self) {
set(value, forKey: key)
return
@ -344,6 +301,7 @@ extension UserDefaults {
@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, *)
private func _set<Value: NSSecureCoding>(_ key: String, to value: Value) {
// TODO: Handle nil here too.
if UserDefaults.isNativelySupportedType(Value.self) {
set(value, forKey: key)
return
@ -367,18 +325,6 @@ extension UserDefaults {
}
}
public subscript<Value: Codable>(key: Defaults.OptionalKey<Value>) -> Value? {
get { _get(key.name) }
set {
guard let value = newValue else {
set(nil, forKey: key.name)
return
}
_set(key.name, to: value)
}
}
@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 subscript<Value: NSSecureCoding>(key: Defaults.NSSecureCodingOptionalKey<Value>) -> Value? {
get { _get(key.name) }
@ -392,16 +338,23 @@ extension UserDefaults {
}
}
fileprivate static func isNativelySupportedType<Value>(_ type: Value.Type) -> Bool {
fileprivate static func isNativelySupportedType<T>(_ type: T.Type) -> Bool {
switch type {
case
is Bool.Type,
is Bool?.Type, // swiftlint:disable:this discouraged_optional_boolean
is String.Type,
is String?.Type,
is Int.Type,
is Int?.Type,
is Double.Type,
is Double?.Type,
is Float.Type,
is Float?.Type,
is Date.Type,
is Data.Type:
is Date?.Type,
is Data.Type,
is Data?.Type:
return true
default:
return false

View File

@ -60,7 +60,7 @@ extension Defaults {
self.options = options
}
func receive<S>(subscriber: S) where S : Subscriber, DefaultsPublisher.Failure == S.Failure, DefaultsPublisher.Output == S.Input {
func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = DefaultsSubscription(
subscriber: subscriber,
suite: suite,
@ -81,7 +81,7 @@ extension Defaults {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
let publisher = Defaults.publisher(.isUnicornMode).map { $0.newValue }
let publisher = Defaults.publisher(.isUnicornMode).map(\.newValue)
let cancellable = publisher.sink { value in
print(value)
@ -114,20 +114,6 @@ extension Defaults {
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<Value: Codable>(
_ key: Defaults.OptionalKey<Value>,
options: NSKeyValueObservingOptions = [.initial, .old, .new]
) -> AnyPublisher<OptionalKeyChange<Value>, Never> {
let publisher = DefaultsPublisher(suite: key.suite, key: key.name, options: options)
.map { OptionalKeyChange<Value>(change: $0) }
return AnyPublisher(publisher)
}
/**
Returns a type-erased `Publisher` that publishes changes related to the given optional key.
*/
@ -154,29 +140,7 @@ extension Defaults {
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<Value: Codable>(
keys: Defaults.OptionalKey<Value>...,
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)
Defaults.publisher(key, options: options)
.map { _ in () }
.eraseToAnyPublisher()
}.reduce(initial) { (combined, keyPublisher) in
@ -198,7 +162,7 @@ extension Defaults {
let combinedPublisher =
keys.map { key in
return Defaults.publisher(key, options: options)
Defaults.publisher(key, options: options)
.map { _ in () }
.eraseToAnyPublisher()
}.reduce(initial) { (combined, keyPublisher) in
@ -220,7 +184,7 @@ extension Defaults {
let combinedPublisher =
keys.map { key in
return Defaults.publisher(key, options: options)
Defaults.publisher(key, options: options)
.map { _ in () }
.eraseToAnyPublisher()
}.reduce(initial) { (combined, keyPublisher) in

View File

@ -113,22 +113,6 @@ extension Defaults {
}
}
public struct OptionalKeyChange<Value: Codable> {
public let kind: NSKeyValueChange
public let indexes: IndexSet?
public let isPrior: Bool
public let newValue: Value?
public let oldValue: Value?
init(change: BaseChange) {
self.kind = change.kind
self.indexes = change.indexes
self.isPrior = change.isPrior
self.oldValue = deserialize(change.oldValue, to: Value.self)
self.newValue = deserialize(change.newValue, to: Value.self)
}
}
@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 struct NSSecureCodingOptionalKeyChange<Value: NSSecureCoding> {
public let kind: NSKeyValueChange
@ -179,6 +163,7 @@ extension Defaults {
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
self?.invalidate()
})
return self
}
@ -255,34 +240,6 @@ extension Defaults {
return observation
}
/**
Observe an optional defaults key.
```
extension Defaults.Keys {
static let isUnicornMode = OptionalKey<Bool>("isUnicornMode")
}
let observer = Defaults.observe(.isUnicornMode) { change in
print(change.newValue)
//=> Optional(nil)
}
```
*/
public static func observe<Value: Codable>(
_ key: Defaults.OptionalKey<Value>,
options: NSKeyValueObservingOptions = [.initial, .old, .new],
handler: @escaping (OptionalKeyChange<Value>) -> Void
) -> DefaultsObservation {
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
handler(
OptionalKeyChange<Value>(change: change)
)
}
observation.start(options: options)
return observation
}
/**
Observe an optional defaults key.
*/

View File

@ -18,16 +18,19 @@ extension Decodable {
}
}
final class AssociatedObject<T: Any> {
subscript(index: Any) -> T? {
final class ObjectAssociation<T: Any> {
subscript(index: AnyObject) -> T? {
get {
return objc_getAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque()) as! T?
} set {
objc_getAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque()) as! T?
}
set {
objc_setAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque(), newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
/**
Causes a given target object to live at least as long as a given owner object.
*/
@ -46,7 +49,7 @@ final class LifetimeAssociation {
}
}
private static let associatedObjects = AssociatedObject<[ObjectLifetimeTracker]>()
private static let associatedObjects = ObjectAssociation<[ObjectLifetimeTracker]>()
private weak var wrappedObject: ObjectLifetimeTracker?
private weak var owner: AnyObject?
@ -113,3 +116,19 @@ final class LifetimeAssociation {
self.owner = nil
}
}
/// A protocol for making generic type constraints of optionals.
/// - Note: It's intentionally not including `associatedtype Wrapped` as that limits a lot of the use-cases.
public protocol _DefaultsOptionalType: ExpressibleByNilLiteral {
/// This is useful as you can't compare `_OptionalType` to `nil`.
var isNil: Bool { get }
}
extension Optional: _DefaultsOptionalType {
public var isNil: Bool { self == nil }
}
func isOptionalType<T>(_ type: T.Type) -> Bool {
type is _DefaultsOptionalType.Type
}

View File

@ -1,8 +1,8 @@
import Foundation
import XCTest
import Defaults
import CoreData
import Combine
import XCTest
import Defaults
let fixtureURL = URL(string: "https://sindresorhus.com")!
let fixtureURL2 = URL(string: "https://example.com")!
@ -17,7 +17,6 @@ let fixtureDate = Date()
@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, *)
final class ExamplePersistentHistory: NSPersistentHistoryToken {
let value: String
init(value: String) {
@ -34,9 +33,7 @@ final class ExamplePersistentHistory: NSPersistentHistoryToken {
coder.encode(value, forKey: "value")
}
override class var supportsSecureCoding: Bool {
return true
}
override class var supportsSecureCoding: Bool { true }
}
extension Defaults.Keys {
@ -72,7 +69,7 @@ final class DefaultsTests: XCTestCase {
}
func testOptionalKey() {
let key = Defaults.OptionalKey<Bool>("independentOptionalKey")
let key = Defaults.Key<Bool?>("independentOptionalKey")
XCTAssertNil(Defaults[key])
Defaults[key] = true
XCTAssertTrue(Defaults[key]!)
@ -227,7 +224,7 @@ final class DefaultsTests: XCTestCase {
@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 key = Defaults.Key<Bool?>("observeOptionalKey")
let expect = expectation(description: "Observation closure being called")
let publisher = Defaults
@ -348,8 +345,8 @@ final class DefaultsTests: XCTestCase {
@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 key1 = Defaults.Key<Bool?>("observeOptionalKey1")
let key2 = Defaults.Key<Bool?>("observeOptionalKey2")
let expect = expectation(description: "Observation closure being called")
let publisher = Defaults.publisher(keys: key1, key2, options: [.old, .new]).collect(2)
@ -441,7 +438,7 @@ final class DefaultsTests: XCTestCase {
}
func testObserveOptionalKey() {
let key = Defaults.OptionalKey<Bool>("observeOptionalKey")
let key = Defaults.Key<Bool?>("observeOptionalKey")
let expect = expectation(description: "Observation closure being called")
var observation: DefaultsObservation!
@ -558,8 +555,8 @@ final class DefaultsTests: XCTestCase {
let newString1 = "bar1"
let newString2 = "bar2"
let newString3 = "bar3"
let key1 = Defaults.OptionalKey<String>("optionalKey1")
let key2 = Defaults.OptionalKey<String>("optionalKey2")
let key1 = Defaults.Key<String?>("optionalKey1")
let key2 = Defaults.Key<String?>("optionalKey2")
Defaults[key1] = newString1
Defaults[key2] = newString2
Defaults.reset(key1)
@ -578,9 +575,9 @@ final class DefaultsTests: XCTestCase {
let newString1 = "bar1"
let newString2 = "bar2"
let newString3 = "bar3"
let key1 = Defaults.OptionalKey<String>("aoptionalKey1")
let key2 = Defaults.OptionalKey<String>("aoptionalKey2")
let key3 = Defaults.OptionalKey<String>("aoptionalKey3")
let key1 = Defaults.Key<String?>("aoptionalKey1")
let key2 = Defaults.Key<String?>("aoptionalKey2")
let key3 = Defaults.Key<String?>("aoptionalKey3")
Defaults[key1] = newString1
Defaults[key2] = newString2
Defaults[key3] = newString3

View File

@ -2,7 +2,7 @@
> Swifty and modern [UserDefaults](https://developer.apple.com/documentation/foundation/userdefaults)
**Note:** The readme reflects the master branch. [Click here](https://github.com/sindresorhus/Defaults/tree/55ffea9487fb9b559406d909ee31dcd955fe77aa#readme) for docs for the latest version. The code in the master branch cannot be released until Apple fixes [this bug](https://github.com/feedback-assistant/reports/issues/44).
#### Note: The readme reflects the master branch. [Click here](https://github.com/sindresorhus/Defaults/tree/55ffea9487fb9b559406d909ee31dcd955fe77aa#readme) for docs for the latest version. The code in the master branch cannot be released until Apple fixes [this bug](https://github.com/feedback-assistant/reports/issues/44).
It uses `NSUserDefaults` underneath but exposes a type-safe facade with lots of nice conveniences.
@ -80,7 +80,7 @@ You can also declare optional keys for when you don't want to declare a default
```swift
extension Defaults.Keys {
static let name = OptionalKey<Double>("name")
static let name = Key<Double?>("name")
}
if let name = Defaults[.name] {
@ -228,7 +228,7 @@ Defaults[.isUnicornMode]
//=> false
```
This works for `OptionalKey` too, which will be reset back to `nil`.
This works for a `Key` with an optional too, which will be reset back to `nil`.
### It's just `UserDefaults` with sugar
@ -308,16 +308,6 @@ Create a NSSecureCoding key with a default value.
The default value is written to the actual `UserDefaults` and can be used elsewhere. For example, with a Interface Builder binding.
#### `Defaults.OptionalKey` *(alias `Defaults.Keys.OptionalKey`)*
```swift
Defaults.OptionalKey<T>(_ key: String, suite: UserDefaults = .standard)
```
Type: `class`
Create a key with an optional value.
#### `Defaults.NSSecureCodingOptionalKey` *(alias `Defaults.Keys.NSSecureCodingOptionalKey`)*
```swift
@ -333,8 +323,6 @@ Create a NSSecureCoding key with an optional value.
```swift
Defaults.reset<T: Codable>(_ keys: Defaults.Key<T>..., suite: UserDefaults = .standard)
Defaults.reset<T: Codable>(_ keys: [Defaults.Key<T>], suite: UserDefaults = .standard)
Defaults.reset<T: Codable>(_ keys: Defaults.OptionalKey<T>..., suite: UserDefaults = .standard)
Defaults.reset<T: Codable>(_ keys: [Defaults.OptionalKey<T>], suite: UserDefaults = .standard)
Defaults.reset<T: Codable>(_ keys: Defaults.NSSecureCodingKey<T>..., suite: UserDefaults = .standard)
Defaults.reset<T: Codable>(_ keys: [Defaults.NSSecureCodingKey<T>], suite: UserDefaults = .standard)
@ -364,14 +352,6 @@ Defaults.observe<T: NSSecureCoding>(
) -> DefaultsObservation
```
```swift
Defaults.observe<T: Codable>(
_ key: Defaults.OptionalKey<T>,
options: NSKeyValueObservingOptions = [.initial, .old, .new],
handler: @escaping (OptionalKeyChange<T>) -> Void
) -> DefaultsObservation
```
```swift
Defaults.observe<T: NSSecureCoding>(
_ key: Defaults.NSSecureCodingOptionalKey<T>,
@ -402,13 +382,6 @@ Defaults.publisher<T: NSSecureCoding>(
) -> 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>,
@ -438,13 +411,6 @@ Defaults.publisher<T: NSSecureCoding>(
) -> 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>...,