Add support for NSSecureCoding (#27)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
This commit is contained in:
parent
54f970b9d7
commit
89d2d4d353
|
@ -4,8 +4,15 @@ import Foundation
|
||||||
public final class Defaults {
|
public final class Defaults {
|
||||||
public class Keys {
|
public class Keys {
|
||||||
public typealias Key = Defaults.Key
|
public typealias Key = Defaults.Key
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public typealias NSSecureCodingKey = Defaults.NSSecureCodingKey
|
||||||
|
|
||||||
public typealias OptionalKey = Defaults.OptionalKey
|
public typealias OptionalKey = Defaults.OptionalKey
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public typealias NSSecureCodingOptionalKey = Defaults.NSSecureCodingOptionalKey
|
||||||
|
|
||||||
fileprivate init() {}
|
fileprivate init() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +38,29 @@ public final class Defaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public final class NSSecureCodingKey<T: NSSecureCoding>: Keys {
|
||||||
|
public let name: String
|
||||||
|
public let defaultValue: T
|
||||||
|
public let suite: UserDefaults
|
||||||
|
|
||||||
|
/// Create a defaults key.
|
||||||
|
public init(_ key: String, default defaultValue: T, suite: UserDefaults = .standard) {
|
||||||
|
self.name = key
|
||||||
|
self.defaultValue = defaultValue
|
||||||
|
self.suite = suite
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
// Sets the default value in the actual UserDefaults, so it can be used in other contexts, like binding.
|
||||||
|
if UserDefaults.isNativelySupportedType(T.self) {
|
||||||
|
suite.register(defaults: [key: defaultValue])
|
||||||
|
} else if let value = try? NSKeyedArchiver.archivedData(withRootObject: defaultValue, requiringSecureCoding: true) {
|
||||||
|
suite.register(defaults: [key: value])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public final class OptionalKey<T: Codable>: Keys {
|
public final class OptionalKey<T: Codable>: Keys {
|
||||||
public let name: String
|
public let name: String
|
||||||
public let suite: UserDefaults
|
public let suite: UserDefaults
|
||||||
|
@ -42,6 +72,18 @@ public final class Defaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public final class NSSecureCodingOptionalKey<T: NSSecureCoding>: 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate init() {}
|
fileprivate init() {}
|
||||||
|
|
||||||
/// Access a defaults value using a `Defaults.Key`.
|
/// Access a defaults value using a `Defaults.Key`.
|
||||||
|
@ -52,6 +94,15 @@ public final class Defaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access a defaults value using a `Defaults.NSSecureCodingKey`.
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public static subscript<T: NSSecureCoding>(key: NSSecureCodingKey<T>) -> T {
|
||||||
|
get { key.suite[key] }
|
||||||
|
set {
|
||||||
|
key.suite[key] = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Access a defaults value using a `Defaults.OptionalKey`.
|
/// Access a defaults value using a `Defaults.OptionalKey`.
|
||||||
public static subscript<T: Codable>(key: OptionalKey<T>) -> T? {
|
public static subscript<T: Codable>(key: OptionalKey<T>) -> T? {
|
||||||
get { key.suite[key] }
|
get { key.suite[key] }
|
||||||
|
@ -59,6 +110,15 @@ public final class Defaults {
|
||||||
key.suite[key] = newValue
|
key.suite[key] = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access a defaults value using a `Defaults.OptionalKey`.
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public static subscript<T: NSSecureCoding>(key: NSSecureCodingOptionalKey<T>) -> T? {
|
||||||
|
get { key.suite[key] }
|
||||||
|
set {
|
||||||
|
key.suite[key] = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Reset the given keys back to their default values.
|
Reset the given keys back to their default values.
|
||||||
|
@ -83,6 +143,17 @@ public final class Defaults {
|
||||||
public static func reset<T: Codable>(_ keys: Key<T>..., suite: UserDefaults = .standard) {
|
public static func reset<T: Codable>(_ keys: Key<T>..., suite: UserDefaults = .standard) {
|
||||||
reset(keys, suite: suite)
|
reset(keys, suite: suite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Reset the given keys back to their default values.
|
||||||
|
|
||||||
|
- Parameter keys: Keys to reset.
|
||||||
|
- Parameter suite: `UserDefaults` suite.
|
||||||
|
*/
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public static func reset<T: NSSecureCoding>(_ keys: NSSecureCodingKey<T>..., suite: UserDefaults = .standard) {
|
||||||
|
reset(keys, suite: suite)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Reset the given array of keys back to their default values.
|
Reset the given array of keys back to their default values.
|
||||||
|
@ -109,6 +180,19 @@ public final class Defaults {
|
||||||
key.suite[key] = key.defaultValue
|
key.suite[key] = key.defaultValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Reset the given array of keys back to their default values.
|
||||||
|
|
||||||
|
- Parameter keys: Keys to reset.
|
||||||
|
- Parameter suite: `UserDefaults` suite.
|
||||||
|
*/
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public static func reset<T: NSSecureCoding>(_ keys: [NSSecureCodingKey<T>], suite: UserDefaults = .standard) {
|
||||||
|
for key in keys {
|
||||||
|
key.suite[key] = key.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Reset the given optional keys back to `nil`.
|
Reset the given optional keys back to `nil`.
|
||||||
|
@ -132,6 +216,18 @@ public final class Defaults {
|
||||||
public static func reset<T: Codable>(_ keys: OptionalKey<T>..., suite: UserDefaults = .standard) {
|
public static func reset<T: Codable>(_ keys: OptionalKey<T>..., suite: UserDefaults = .standard) {
|
||||||
reset(keys, suite: suite)
|
reset(keys, suite: suite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Reset the given optional keys back to `nil`.
|
||||||
|
|
||||||
|
- Parameter keys: Keys to reset.
|
||||||
|
- Parameter suite: `UserDefaults` suite.
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public static func reset<T: NSSecureCoding>(_ keys: NSSecureCodingOptionalKey<T>..., suite: UserDefaults = .standard) {
|
||||||
|
reset(keys, suite: suite)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Reset the given array of optional keys back to `nil`.
|
Reset the given array of optional keys back to `nil`.
|
||||||
|
@ -158,6 +254,19 @@ public final class Defaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Reset the given array of optional keys back to `nil`.
|
||||||
|
|
||||||
|
- Parameter keys: Keys to reset.
|
||||||
|
- Parameter suite: `UserDefaults` suite.
|
||||||
|
*/
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public static func reset<T: NSSecureCoding>(_ keys: [NSSecureCodingOptionalKey<T>], suite: UserDefaults = .standard) {
|
||||||
|
for key in keys {
|
||||||
|
key.suite[key] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Remove all entries from the `UserDefaults` suite.
|
Remove all entries from the `UserDefaults` suite.
|
||||||
*/
|
*/
|
||||||
|
@ -190,6 +299,27 @@ extension UserDefaults {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
private func _get<T: NSSecureCoding>(_ key: String) -> T? {
|
||||||
|
if UserDefaults.isNativelySupportedType(T.self) {
|
||||||
|
return object(forKey: key) as? T
|
||||||
|
}
|
||||||
|
|
||||||
|
guard
|
||||||
|
let data = data(forKey: key)
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
return try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? T
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate func _encode<T: Codable>(_ value: T) -> String? {
|
fileprivate func _encode<T: Codable>(_ value: T) -> String? {
|
||||||
do {
|
do {
|
||||||
// Some codable values like URL and enum are encoded as a top-level
|
// Some codable values like URL and enum are encoded as a top-level
|
||||||
|
@ -212,6 +342,16 @@ extension UserDefaults {
|
||||||
set(_encode(value), forKey: key)
|
set(_encode(value), forKey: key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
private func _set<T: NSSecureCoding>(_ key: String, to value: T) {
|
||||||
|
if UserDefaults.isNativelySupportedType(T.self) {
|
||||||
|
set(value, forKey: key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
set(try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: true), forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
public subscript<T: Codable>(key: Defaults.Key<T>) -> T {
|
public subscript<T: Codable>(key: Defaults.Key<T>) -> T {
|
||||||
get { _get(key.name) ?? key.defaultValue }
|
get { _get(key.name) ?? key.defaultValue }
|
||||||
set {
|
set {
|
||||||
|
@ -219,6 +359,14 @@ extension UserDefaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public subscript<T: NSSecureCoding>(key: Defaults.NSSecureCodingKey<T>) -> T {
|
||||||
|
get { _get(key.name) ?? key.defaultValue }
|
||||||
|
set {
|
||||||
|
_set(key.name, to: newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public subscript<T: Codable>(key: Defaults.OptionalKey<T>) -> T? {
|
public subscript<T: Codable>(key: Defaults.OptionalKey<T>) -> T? {
|
||||||
get { _get(key.name) }
|
get { _get(key.name) }
|
||||||
set {
|
set {
|
||||||
|
@ -231,6 +379,19 @@ extension UserDefaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public subscript<T: NSSecureCoding>(key: Defaults.NSSecureCodingOptionalKey<T>) -> T? {
|
||||||
|
get { _get(key.name) }
|
||||||
|
set {
|
||||||
|
guard let value = newValue else {
|
||||||
|
set(nil, forKey: key.name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_set(key.name, to: value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate static func isNativelySupportedType<T>(_ type: T.Type) -> Bool {
|
fileprivate static func isNativelySupportedType<T>(_ type: T.Type) -> Bool {
|
||||||
switch type {
|
switch type {
|
||||||
case is Bool.Type,
|
case is Bool.Type,
|
||||||
|
|
|
@ -43,6 +43,27 @@ extension Defaults {
|
||||||
return [T].init(jsonString: "\([value])")?.first
|
return [T].init(jsonString: "\([value])")?.first
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
private static func deserialize<T: NSSecureCoding>(_ value: Any?, to type: T.Type) -> T? {
|
||||||
|
guard
|
||||||
|
let value = value,
|
||||||
|
!(value is NSNull)
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This handles the case where the value was a plist value using `isNativelySupportedType`
|
||||||
|
if let value = value as? T {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let dataValue = value as? Data else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(dataValue) as? T
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate final class BaseChange {
|
fileprivate final class BaseChange {
|
||||||
fileprivate let kind: NSKeyValueChange
|
fileprivate let kind: NSKeyValueChange
|
||||||
fileprivate let indexes: IndexSet?
|
fileprivate let indexes: IndexSet?
|
||||||
|
@ -75,6 +96,23 @@ extension Defaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public struct NSSecureCodingKeyChange<T: NSSecureCoding> {
|
||||||
|
public let kind: NSKeyValueChange
|
||||||
|
public let indexes: IndexSet?
|
||||||
|
public let isPrior: Bool
|
||||||
|
public let newValue: T
|
||||||
|
public let oldValue: T
|
||||||
|
|
||||||
|
fileprivate init(change: BaseChange, defaultValue: T) {
|
||||||
|
self.kind = change.kind
|
||||||
|
self.indexes = change.indexes
|
||||||
|
self.isPrior = change.isPrior
|
||||||
|
self.oldValue = deserialize(change.oldValue, to: T.self) ?? defaultValue
|
||||||
|
self.newValue = deserialize(change.newValue, to: T.self) ?? defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct OptionalKeyChange<T: Codable> {
|
public struct OptionalKeyChange<T: Codable> {
|
||||||
public let kind: NSKeyValueChange
|
public let kind: NSKeyValueChange
|
||||||
public let indexes: IndexSet?
|
public let indexes: IndexSet?
|
||||||
|
@ -91,6 +129,23 @@ extension Defaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public struct NSSecureCodingOptionalKeyChange<T: NSSecureCoding> {
|
||||||
|
public let kind: NSKeyValueChange
|
||||||
|
public let indexes: IndexSet?
|
||||||
|
public let isPrior: Bool
|
||||||
|
public let newValue: T?
|
||||||
|
public let oldValue: T?
|
||||||
|
|
||||||
|
fileprivate init(change: BaseChange) {
|
||||||
|
self.kind = change.kind
|
||||||
|
self.indexes = change.indexes
|
||||||
|
self.isPrior = change.isPrior
|
||||||
|
self.oldValue = deserialize(change.oldValue, to: T.self)
|
||||||
|
self.newValue = deserialize(change.newValue, to: T.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class UserDefaultsKeyObservation: NSObject, DefaultsObservation {
|
private final class UserDefaultsKeyObservation: NSObject, DefaultsObservation {
|
||||||
fileprivate typealias Callback = (BaseChange) -> Void
|
fileprivate typealias Callback = (BaseChange) -> Void
|
||||||
|
|
||||||
|
@ -182,6 +237,24 @@ extension Defaults {
|
||||||
return observation
|
return observation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Observe a defaults key.
|
||||||
|
*/
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public static func observe<T: NSSecureCoding>(
|
||||||
|
_ key: Defaults.NSSecureCodingKey<T>,
|
||||||
|
options: NSKeyValueObservingOptions = [.initial, .old, .new],
|
||||||
|
handler: @escaping (NSSecureCodingKeyChange<T>) -> Void
|
||||||
|
) -> DefaultsObservation {
|
||||||
|
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
|
||||||
|
handler(
|
||||||
|
NSSecureCodingKeyChange<T>(change: change, defaultValue: key.defaultValue)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
observation.start(options: options)
|
||||||
|
return observation
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Observe an optional defaults key.
|
Observe an optional defaults key.
|
||||||
|
|
||||||
|
@ -209,4 +282,22 @@ extension Defaults {
|
||||||
observation.start(options: options)
|
observation.start(options: options)
|
||||||
return observation
|
return observation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Observe an optional defaults key.
|
||||||
|
*/
|
||||||
|
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
|
||||||
|
public static func observe<T: NSSecureCoding>(
|
||||||
|
_ key: Defaults.NSSecureCodingOptionalKey<T>,
|
||||||
|
options: NSKeyValueObservingOptions = [.initial, .old, .new],
|
||||||
|
handler: @escaping (NSSecureCodingOptionalKeyChange<T>) -> Void
|
||||||
|
) -> DefaultsObservation {
|
||||||
|
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
|
||||||
|
handler(
|
||||||
|
NSSecureCodingOptionalKeyChange<T>(change: change)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
observation.start(options: options)
|
||||||
|
return observation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import XCTest
|
import XCTest
|
||||||
import Defaults
|
import Defaults
|
||||||
|
import CoreData
|
||||||
|
|
||||||
let fixtureURL = URL(string: "https://sindresorhus.com")!
|
let fixtureURL = URL(string: "https://sindresorhus.com")!
|
||||||
let fixtureURL2 = URL(string: "https://example.com")!
|
let fixtureURL2 = URL(string: "https://example.com")!
|
||||||
|
@ -13,12 +14,42 @@ enum FixtureEnum: String, Codable {
|
||||||
|
|
||||||
let fixtureDate = Date()
|
let fixtureDate = Date()
|
||||||
|
|
||||||
|
@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *)
|
||||||
|
final class ExamplePersistentHistory: NSPersistentHistoryToken {
|
||||||
|
|
||||||
|
let value: String
|
||||||
|
|
||||||
|
init(value: String) {
|
||||||
|
self.value = value
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
self.value = coder.decodeObject(forKey: "value") as! String
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func encode(with coder: NSCoder) {
|
||||||
|
coder.encode(value, forKey: "value")
|
||||||
|
}
|
||||||
|
|
||||||
|
override class var supportsSecureCoding: Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension Defaults.Keys {
|
extension Defaults.Keys {
|
||||||
static let key = Key<Bool>("key", default: false)
|
static let key = Key<Bool>("key", default: false)
|
||||||
static let url = Key<URL>("url", default: fixtureURL)
|
static let url = Key<URL>("url", default: fixtureURL)
|
||||||
static let `enum` = Key<FixtureEnum>("enum", default: .oneHour)
|
static let `enum` = Key<FixtureEnum>("enum", default: .oneHour)
|
||||||
static let data = Key<Data>("data", default: Data([]))
|
static let data = Key<Data>("data", default: Data([]))
|
||||||
static let date = Key<Date>("date", default: fixtureDate)
|
static let date = Key<Date>("date", default: fixtureDate)
|
||||||
|
|
||||||
|
// NSSecureCoding
|
||||||
|
@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *)
|
||||||
|
static let persistentHistoryValue = ExamplePersistentHistory(value: "ExampleToken")
|
||||||
|
@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *)
|
||||||
|
static let persistentHistory = NSSecureCodingKey<ExamplePersistentHistory>("persistentHistory", default: persistentHistoryValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class DefaultsTests: XCTestCase {
|
final class DefaultsTests: XCTestCase {
|
||||||
|
@ -75,6 +106,14 @@ final class DefaultsTests: XCTestCase {
|
||||||
XCTAssertTrue(Defaults[.key])
|
XCTAssertTrue(Defaults[.key])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *)
|
||||||
|
func testNSSecureCodingKeys() {
|
||||||
|
XCTAssertEqual(Defaults.Keys.persistentHistoryValue.value, Defaults[.persistentHistory].value)
|
||||||
|
let newPersistentHistory = ExamplePersistentHistory(value: "NewValue")
|
||||||
|
Defaults[.persistentHistory] = newPersistentHistory
|
||||||
|
XCTAssertEqual(newPersistentHistory.value, Defaults[.persistentHistory].value)
|
||||||
|
}
|
||||||
|
|
||||||
func testUrlType() {
|
func testUrlType() {
|
||||||
XCTAssertEqual(Defaults[.url], fixtureURL)
|
XCTAssertEqual(Defaults[.url], fixtureURL)
|
||||||
|
|
||||||
|
@ -143,6 +182,24 @@ final class DefaultsTests: XCTestCase {
|
||||||
waitForExpectations(timeout: 10)
|
waitForExpectations(timeout: 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *)
|
||||||
|
func testObserveNSSecureCodingKey() {
|
||||||
|
let key = Defaults.NSSecureCodingKey<ExamplePersistentHistory>("observeNSSecureCodingKey", default: ExamplePersistentHistory(value: "TestValue"))
|
||||||
|
let expect = expectation(description: "Observation closure being called")
|
||||||
|
|
||||||
|
var observation: DefaultsObservation!
|
||||||
|
observation = Defaults.observe(key, options: [.old, .new]) { change in
|
||||||
|
XCTAssertEqual(change.oldValue.value, "TestValue")
|
||||||
|
XCTAssertEqual(change.newValue.value, "NewTestValue")
|
||||||
|
observation.invalidate()
|
||||||
|
expect.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
Defaults[key] = ExamplePersistentHistory(value: "NewTestValue")
|
||||||
|
|
||||||
|
waitForExpectations(timeout: 10)
|
||||||
|
}
|
||||||
|
|
||||||
func testObserveOptionalKey() {
|
func testObserveOptionalKey() {
|
||||||
let key = Defaults.OptionalKey<Bool>("observeOptionalKey")
|
let key = Defaults.OptionalKey<Bool>("observeOptionalKey")
|
||||||
let expect = expectation(description: "Observation closure being called")
|
let expect = expectation(description: "Observation closure being called")
|
||||||
|
@ -160,6 +217,24 @@ final class DefaultsTests: XCTestCase {
|
||||||
waitForExpectations(timeout: 10)
|
waitForExpectations(timeout: 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *)
|
||||||
|
func testObserveNSSecureCodingOptionalKey() {
|
||||||
|
let key = Defaults.NSSecureCodingOptionalKey<ExamplePersistentHistory>("observeNSSecureCodingOptionalKey")
|
||||||
|
let expect = expectation(description: "Observation closure being called")
|
||||||
|
|
||||||
|
var observation: DefaultsObservation!
|
||||||
|
observation = Defaults.observe(key, options: [.old, .new]) { change in
|
||||||
|
XCTAssertNil(change.oldValue)
|
||||||
|
XCTAssertEqual(change.newValue?.value, "NewOptionalValue")
|
||||||
|
observation.invalidate()
|
||||||
|
expect.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
Defaults[key] = ExamplePersistentHistory(value: "NewOptionalValue")
|
||||||
|
|
||||||
|
waitForExpectations(timeout: 10)
|
||||||
|
}
|
||||||
|
|
||||||
func testObserveKeyURL() {
|
func testObserveKeyURL() {
|
||||||
let fixtureURL = URL(string: "https://sindresorhus.com")!
|
let fixtureURL = URL(string: "https://sindresorhus.com")!
|
||||||
let fixtureURL2 = URL(string: "https://example.com")!
|
let fixtureURL2 = URL(string: "https://example.com")!
|
||||||
|
@ -199,8 +274,10 @@ final class DefaultsTests: XCTestCase {
|
||||||
func testResetKey() {
|
func testResetKey() {
|
||||||
let defaultString1 = "foo1"
|
let defaultString1 = "foo1"
|
||||||
let defaultString2 = "foo2"
|
let defaultString2 = "foo2"
|
||||||
|
let defaultString3 = "foo3"
|
||||||
let newString1 = "bar1"
|
let newString1 = "bar1"
|
||||||
let newString2 = "bar2"
|
let newString2 = "bar2"
|
||||||
|
let newString3 = "bar3"
|
||||||
let key1 = Defaults.Key<String>("key1", default: defaultString1)
|
let key1 = Defaults.Key<String>("key1", default: defaultString1)
|
||||||
let key2 = Defaults.Key<String>("key2", default: defaultString2)
|
let key2 = Defaults.Key<String>("key2", default: defaultString2)
|
||||||
Defaults[key1] = newString1
|
Defaults[key1] = newString1
|
||||||
|
@ -208,6 +285,14 @@ final class DefaultsTests: XCTestCase {
|
||||||
Defaults.reset(key1)
|
Defaults.reset(key1)
|
||||||
XCTAssertEqual(Defaults[key1], defaultString1)
|
XCTAssertEqual(Defaults[key1], defaultString1)
|
||||||
XCTAssertEqual(Defaults[key2], newString2)
|
XCTAssertEqual(Defaults[key2], newString2)
|
||||||
|
|
||||||
|
if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) {
|
||||||
|
let key3 = Defaults.NSSecureCodingKey<ExamplePersistentHistory>("key3", default: ExamplePersistentHistory(value: defaultString3))
|
||||||
|
Defaults[key3] = ExamplePersistentHistory(value: newString3)
|
||||||
|
Defaults.reset(key3)
|
||||||
|
|
||||||
|
XCTAssertEqual(Defaults[key3].value, defaultString3)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testResetKeyArray() {
|
func testResetKeyArray() {
|
||||||
|
@ -232,6 +317,7 @@ final class DefaultsTests: XCTestCase {
|
||||||
func testResetOptionalKey() {
|
func testResetOptionalKey() {
|
||||||
let newString1 = "bar1"
|
let newString1 = "bar1"
|
||||||
let newString2 = "bar2"
|
let newString2 = "bar2"
|
||||||
|
let newString3 = "bar3"
|
||||||
let key1 = Defaults.OptionalKey<String>("optionalKey1")
|
let key1 = Defaults.OptionalKey<String>("optionalKey1")
|
||||||
let key2 = Defaults.OptionalKey<String>("optionalKey2")
|
let key2 = Defaults.OptionalKey<String>("optionalKey2")
|
||||||
Defaults[key1] = newString1
|
Defaults[key1] = newString1
|
||||||
|
@ -239,6 +325,13 @@ final class DefaultsTests: XCTestCase {
|
||||||
Defaults.reset(key1)
|
Defaults.reset(key1)
|
||||||
XCTAssertEqual(Defaults[key1], nil)
|
XCTAssertEqual(Defaults[key1], nil)
|
||||||
XCTAssertEqual(Defaults[key2], newString2)
|
XCTAssertEqual(Defaults[key2], newString2)
|
||||||
|
|
||||||
|
if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) {
|
||||||
|
let key3 = Defaults.NSSecureCodingOptionalKey<ExamplePersistentHistory>("optionalKey3")
|
||||||
|
Defaults[key3] = ExamplePersistentHistory(value: newString3)
|
||||||
|
Defaults.reset(key3)
|
||||||
|
XCTAssertEqual(Defaults[key3], nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testResetOptionalKeyArray() {
|
func testResetOptionalKeyArray() {
|
||||||
|
|
66
readme.md
66
readme.md
|
@ -9,6 +9,7 @@ This package is used in production by apps like [Gifski](https://github.com/sind
|
||||||
|
|
||||||
- **Strongly typed:** You declare the type and default value upfront.
|
- **Strongly typed:** You declare the type and default value upfront.
|
||||||
- **Codable support:** You can store any [Codable](https://developer.apple.com/documentation/swift/codable) value, like an enum.
|
- **Codable support:** You can store any [Codable](https://developer.apple.com/documentation/swift/codable) value, like an enum.
|
||||||
|
- **NSSecureCoding support:** You can store any [NSSecureCoding](https://developer.apple.com/documentation/foundation/nssecurecoding) value.
|
||||||
- **Debuggable:** The data is stored as JSON-serialized values.
|
- **Debuggable:** The data is stored as JSON-serialized values.
|
||||||
- **Observation:** Observe changes to keys.
|
- **Observation:** Observe changes to keys.
|
||||||
- **Lightweight:** It's only ~300 lines of code.
|
- **Lightweight:** It's only ~300 lines of code.
|
||||||
|
@ -88,6 +89,28 @@ if let name = Defaults[.name] {
|
||||||
|
|
||||||
The default value is then `nil`.
|
The default value is then `nil`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If you have `NSSecureCoding` classes which you want to save, you can use them as follows:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
extension Defaults.Keys {
|
||||||
|
static let someSecureCoding = NSSecureCodingKey<SomeNSSecureCodingClass>("someSecureCoding", default: SomeNSSecureCodingClass(string: "Default", int: 5, bool: true))
|
||||||
|
static let someOptionalSecureCoding = NSSecureCodingOptionalKey<Double>("someOptionalSecureCoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
Defaults[.someSecureCoding].string
|
||||||
|
//=> "Default"
|
||||||
|
|
||||||
|
Defaults[.someSecureCoding].int
|
||||||
|
//=> 5
|
||||||
|
|
||||||
|
Defaults[.someSecureCoding].bool
|
||||||
|
//=> true
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use those keys just like in all the other examples. The return value will be your `NSSecureCoding` class.
|
||||||
|
|
||||||
### Enum example
|
### Enum example
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
|
@ -248,6 +271,18 @@ Create a 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.
|
The default value is written to the actual `UserDefaults` and can be used elsewhere. For example, with a Interface Builder binding.
|
||||||
|
|
||||||
|
#### `Defaults.NSSecureCodingKey` *(alias `Defaults.Keys.NSSecureCodingKey`)*
|
||||||
|
|
||||||
|
```swift
|
||||||
|
Defaults.NSSecureCodingKey<T>(_ key: String, default: T, suite: UserDefaults = .standard)
|
||||||
|
```
|
||||||
|
|
||||||
|
Type: `class`
|
||||||
|
|
||||||
|
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`)*
|
#### `Defaults.OptionalKey` *(alias `Defaults.Keys.OptionalKey`)*
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
|
@ -258,6 +293,16 @@ Type: `class`
|
||||||
|
|
||||||
Create a key with an optional value.
|
Create a key with an optional value.
|
||||||
|
|
||||||
|
#### `Defaults.NSSecureCodingOptionalKey` *(alias `Defaults.Keys.NSSecureCodingOptionalKey`)*
|
||||||
|
|
||||||
|
```swift
|
||||||
|
Defaults.NSSecureCodingOptionalKey<T>(_ key: String, suite: UserDefaults = .standard)
|
||||||
|
```
|
||||||
|
|
||||||
|
Type: `class`
|
||||||
|
|
||||||
|
Create a NSSecureCoding key with an optional value.
|
||||||
|
|
||||||
#### `Defaults.reset`
|
#### `Defaults.reset`
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
|
@ -265,6 +310,11 @@ Defaults.reset<T: Codable>(_ keys: Defaults.Key<T>..., suite: UserDefaults = .st
|
||||||
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.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)
|
||||||
|
Defaults.reset<T: Codable>(_ keys: Defaults.NSSecureCodingOptionalKey<T>..., suite: UserDefaults = .standard)
|
||||||
|
Defaults.reset<T: Codable>(_ keys: [Defaults.NSSecureCodingOptionalKey<T>], suite: UserDefaults = .standard)
|
||||||
```
|
```
|
||||||
|
|
||||||
Type: `func`
|
Type: `func`
|
||||||
|
@ -281,6 +331,14 @@ Defaults.observe<T: Codable>(
|
||||||
) -> DefaultsObservation
|
) -> DefaultsObservation
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```swift
|
||||||
|
Defaults.observe<T: Codable>(
|
||||||
|
_ key: Defaults.NSSecureCodingKey<T>,
|
||||||
|
options: NSKeyValueObservingOptions = [.initial, .old, .new],
|
||||||
|
handler: @escaping (NSSecureCodingKeyChange<T>) -> Void
|
||||||
|
) -> DefaultsObservation
|
||||||
|
```
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
Defaults.observe<T: Codable>(
|
Defaults.observe<T: Codable>(
|
||||||
_ key: Defaults.OptionalKey<T>,
|
_ key: Defaults.OptionalKey<T>,
|
||||||
|
@ -289,6 +347,14 @@ Defaults.observe<T: Codable>(
|
||||||
) -> DefaultsObservation
|
) -> DefaultsObservation
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```swift
|
||||||
|
Defaults.observe<T: Codable>(
|
||||||
|
_ key: Defaults.NSSecureCodingOptionalKey<T>,
|
||||||
|
options: NSKeyValueObservingOptions = [.initial, .old, .new],
|
||||||
|
handler: @escaping (NSSecureCodingOptionalKeyChange<T>) -> Void
|
||||||
|
) -> DefaultsObservation
|
||||||
|
```
|
||||||
|
|
||||||
Type: `func`
|
Type: `func`
|
||||||
|
|
||||||
Observe changes to a key or an optional key.
|
Observe changes to a key or an optional key.
|
||||||
|
|
Loading…
Reference in New Issue