Add support for NSSecureCoding (#27)

Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
This commit is contained in:
Koray Koska 2019-10-30 12:57:31 +01:00 committed by Sindre Sorhus
parent 54f970b9d7
commit 89d2d4d353
4 changed files with 411 additions and 0 deletions

View File

@ -4,8 +4,15 @@ import Foundation
public final class Defaults {
public class Keys {
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
@available(iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
public typealias NSSecureCodingOptionalKey = Defaults.NSSecureCodingOptionalKey
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 let name: String
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() {}
/// 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`.
public static subscript<T: Codable>(key: OptionalKey<T>) -> T? {
get { key.suite[key] }
@ -60,6 +111,15 @@ public final class Defaults {
}
}
/// 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.
@ -84,6 +144,17 @@ public final class Defaults {
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.
@ -110,6 +181,19 @@ public final class Defaults {
}
}
/**
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`.
@ -133,6 +217,18 @@ public final class Defaults {
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`.
@ -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.
*/
@ -190,6 +299,27 @@ extension UserDefaults {
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? {
do {
// Some codable values like URL and enum are encoded as a top-level
@ -212,6 +342,16 @@ extension UserDefaults {
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 {
get { _get(key.name) ?? key.defaultValue }
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? {
get { _get(key.name) }
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 {
switch type {
case is Bool.Type,

View File

@ -43,6 +43,27 @@ extension Defaults {
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 let kind: NSKeyValueChange
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 let kind: NSKeyValueChange
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 {
fileprivate typealias Callback = (BaseChange) -> Void
@ -182,6 +237,24 @@ extension Defaults {
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.
@ -209,4 +282,22 @@ extension Defaults {
observation.start(options: options)
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
}
}

View File

@ -1,6 +1,7 @@
import Foundation
import XCTest
import Defaults
import CoreData
let fixtureURL = URL(string: "https://sindresorhus.com")!
let fixtureURL2 = URL(string: "https://example.com")!
@ -13,12 +14,42 @@ enum FixtureEnum: String, Codable {
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 {
static let key = Key<Bool>("key", default: false)
static let url = Key<URL>("url", default: fixtureURL)
static let `enum` = Key<FixtureEnum>("enum", default: .oneHour)
static let data = Key<Data>("data", default: Data([]))
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 {
@ -75,6 +106,14 @@ final class DefaultsTests: XCTestCase {
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() {
XCTAssertEqual(Defaults[.url], fixtureURL)
@ -143,6 +182,24 @@ final class DefaultsTests: XCTestCase {
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() {
let key = Defaults.OptionalKey<Bool>("observeOptionalKey")
let expect = expectation(description: "Observation closure being called")
@ -160,6 +217,24 @@ final class DefaultsTests: XCTestCase {
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() {
let fixtureURL = URL(string: "https://sindresorhus.com")!
let fixtureURL2 = URL(string: "https://example.com")!
@ -199,8 +274,10 @@ final class DefaultsTests: XCTestCase {
func testResetKey() {
let defaultString1 = "foo1"
let defaultString2 = "foo2"
let defaultString3 = "foo3"
let newString1 = "bar1"
let newString2 = "bar2"
let newString3 = "bar3"
let key1 = Defaults.Key<String>("key1", default: defaultString1)
let key2 = Defaults.Key<String>("key2", default: defaultString2)
Defaults[key1] = newString1
@ -208,6 +285,14 @@ final class DefaultsTests: XCTestCase {
Defaults.reset(key1)
XCTAssertEqual(Defaults[key1], defaultString1)
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() {
@ -232,6 +317,7 @@ final class DefaultsTests: XCTestCase {
func testResetOptionalKey() {
let newString1 = "bar1"
let newString2 = "bar2"
let newString3 = "bar3"
let key1 = Defaults.OptionalKey<String>("optionalKey1")
let key2 = Defaults.OptionalKey<String>("optionalKey2")
Defaults[key1] = newString1
@ -239,6 +325,13 @@ final class DefaultsTests: XCTestCase {
Defaults.reset(key1)
XCTAssertEqual(Defaults[key1], nil)
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() {

View File

@ -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.
- **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.
- **Observation:** Observe changes to keys.
- **Lightweight:** It's only ~300 lines of code.
@ -88,6 +89,28 @@ if let name = Defaults[.name] {
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
```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.
#### `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`)*
```swift
@ -258,6 +293,16 @@ Type: `class`
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`
```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.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`
@ -281,6 +331,14 @@ Defaults.observe<T: Codable>(
) -> DefaultsObservation
```
```swift
Defaults.observe<T: Codable>(
_ key: Defaults.NSSecureCodingKey<T>,
options: NSKeyValueObservingOptions = [.initial, .old, .new],
handler: @escaping (NSSecureCodingKeyChange<T>) -> Void
) -> DefaultsObservation
```
```swift
Defaults.observe<T: Codable>(
_ key: Defaults.OptionalKey<T>,
@ -289,6 +347,14 @@ Defaults.observe<T: Codable>(
) -> DefaultsObservation
```
```swift
Defaults.observe<T: Codable>(
_ key: Defaults.NSSecureCodingOptionalKey<T>,
options: NSKeyValueObservingOptions = [.initial, .old, .new],
handler: @escaping (NSSecureCodingOptionalKeyChange<T>) -> Void
) -> DefaultsObservation
```
Type: `func`
Observe changes to a key or an optional key.