From 89d2d4d3537a8d5d27dfff5d42e53f6f86d1eaa5 Mon Sep 17 00:00:00 2001 From: Koray Koska Date: Wed, 30 Oct 2019 12:57:31 +0100 Subject: [PATCH] Add support for NSSecureCoding (#27) Co-authored-by: Sindre Sorhus --- Sources/Defaults/Defaults.swift | 161 ++++++++++++++++++++++++ Sources/Defaults/Observation.swift | 91 ++++++++++++++ Tests/DefaultsTests/DefaultsTests.swift | 93 ++++++++++++++ readme.md | 66 ++++++++++ 4 files changed, 411 insertions(+) diff --git a/Sources/Defaults/Defaults.swift b/Sources/Defaults/Defaults.swift index f69cf72..620e08b 100644 --- a/Sources/Defaults/Defaults.swift +++ b/Sources/Defaults/Defaults.swift @@ -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: 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: 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: 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(key: NSSecureCodingKey) -> T { + get { key.suite[key] } + set { + key.suite[key] = newValue + } + } + /// Access a defaults value using a `Defaults.OptionalKey`. public static subscript(key: OptionalKey) -> T? { get { key.suite[key] } @@ -59,6 +110,15 @@ public final class Defaults { 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(key: NSSecureCodingOptionalKey) -> T? { + get { key.suite[key] } + set { + key.suite[key] = newValue + } + } /** Reset the given keys back to their default values. @@ -83,6 +143,17 @@ public final class Defaults { public static func reset(_ keys: Key..., suite: UserDefaults = .standard) { 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(_ keys: NSSecureCodingKey..., suite: UserDefaults = .standard) { + reset(keys, suite: suite) + } /** Reset the given array of keys back to their default values. @@ -109,6 +180,19 @@ public final class Defaults { 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(_ keys: [NSSecureCodingKey], suite: UserDefaults = .standard) { + for key in keys { + key.suite[key] = key.defaultValue + } + } /** Reset the given optional keys back to `nil`. @@ -132,6 +216,18 @@ public final class Defaults { public static func reset(_ keys: OptionalKey..., suite: UserDefaults = .standard) { 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(_ keys: NSSecureCodingOptionalKey..., 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(_ keys: [NSSecureCodingOptionalKey], 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(_ 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(_ 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(_ 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(key: Defaults.Key) -> 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(key: Defaults.NSSecureCodingKey) -> T { + get { _get(key.name) ?? key.defaultValue } + set { + _set(key.name, to: newValue) + } + } + public subscript(key: Defaults.OptionalKey) -> 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(key: Defaults.NSSecureCodingOptionalKey) -> 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(_ type: T.Type) -> Bool { switch type { case is Bool.Type, diff --git a/Sources/Defaults/Observation.swift b/Sources/Defaults/Observation.swift index 4cb3bd5..365fb0c 100644 --- a/Sources/Defaults/Observation.swift +++ b/Sources/Defaults/Observation.swift @@ -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(_ 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 { + 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 { 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 { + 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( + _ key: Defaults.NSSecureCodingKey, + options: NSKeyValueObservingOptions = [.initial, .old, .new], + handler: @escaping (NSSecureCodingKeyChange) -> Void + ) -> DefaultsObservation { + let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in + handler( + NSSecureCodingKeyChange(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( + _ key: Defaults.NSSecureCodingOptionalKey, + options: NSKeyValueObservingOptions = [.initial, .old, .new], + handler: @escaping (NSSecureCodingOptionalKeyChange) -> Void + ) -> DefaultsObservation { + let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in + handler( + NSSecureCodingOptionalKeyChange(change: change) + ) + } + observation.start(options: options) + return observation + } } diff --git a/Tests/DefaultsTests/DefaultsTests.swift b/Tests/DefaultsTests/DefaultsTests.swift index 66fdccf..1f77495 100644 --- a/Tests/DefaultsTests/DefaultsTests.swift +++ b/Tests/DefaultsTests/DefaultsTests.swift @@ -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("key", default: false) static let url = Key("url", default: fixtureURL) static let `enum` = Key("enum", default: .oneHour) static let data = Key("data", default: Data([])) static let date = Key("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("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("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("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("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("key1", default: defaultString1) let key2 = Defaults.Key("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("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("optionalKey1") let key2 = Defaults.OptionalKey("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("optionalKey3") + Defaults[key3] = ExamplePersistentHistory(value: newString3) + Defaults.reset(key3) + XCTAssertEqual(Defaults[key3], nil) + } } func testResetOptionalKeyArray() { diff --git a/readme.md b/readme.md index 01072e2..8b19ca0 100644 --- a/readme.md +++ b/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. - **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("someSecureCoding", default: SomeNSSecureCodingClass(string: "Default", int: 5, bool: true)) + static let someOptionalSecureCoding = NSSecureCodingOptionalKey("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(_ 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(_ key: String, suite: UserDefaults = .standard) +``` + +Type: `class` + +Create a NSSecureCoding key with an optional value. + #### `Defaults.reset` ```swift @@ -265,6 +310,11 @@ Defaults.reset(_ keys: Defaults.Key..., suite: UserDefaults = .st Defaults.reset(_ keys: [Defaults.Key], suite: UserDefaults = .standard) Defaults.reset(_ keys: Defaults.OptionalKey..., suite: UserDefaults = .standard) Defaults.reset(_ keys: [Defaults.OptionalKey], suite: UserDefaults = .standard) + +Defaults.reset(_ keys: Defaults.NSSecureCodingKey..., suite: UserDefaults = .standard) +Defaults.reset(_ keys: [Defaults.NSSecureCodingKey], suite: UserDefaults = .standard) +Defaults.reset(_ keys: Defaults.NSSecureCodingOptionalKey..., suite: UserDefaults = .standard) +Defaults.reset(_ keys: [Defaults.NSSecureCodingOptionalKey], suite: UserDefaults = .standard) ``` Type: `func` @@ -281,6 +331,14 @@ Defaults.observe( ) -> DefaultsObservation ``` +```swift +Defaults.observe( + _ key: Defaults.NSSecureCodingKey, + options: NSKeyValueObservingOptions = [.initial, .old, .new], + handler: @escaping (NSSecureCodingKeyChange) -> Void +) -> DefaultsObservation +``` + ```swift Defaults.observe( _ key: Defaults.OptionalKey, @@ -289,6 +347,14 @@ Defaults.observe( ) -> DefaultsObservation ``` +```swift +Defaults.observe( + _ key: Defaults.NSSecureCodingOptionalKey, + options: NSKeyValueObservingOptions = [.initial, .old, .new], + handler: @escaping (NSSecureCodingOptionalKeyChange) -> Void +) -> DefaultsObservation +``` + Type: `func` Observe changes to a key or an optional key.