diff --git a/.travis.yml b/.travis.yml index d728b5f..3087b64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ language: swift -osx_image: xcode11.3 +osx_image: xcode11.4 script: xcodebuild test -project Defaults.xcodeproj -scheme Defaults-macOS diff --git a/Defaults.podspec b/Defaults.podspec index 336f980..f4c52ea 100644 --- a/Defaults.podspec +++ b/Defaults.podspec @@ -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' diff --git a/Package.swift b/Package.swift index c0f32c9..cf586e5 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.2 import PackageDescription let package = Package( diff --git a/Sources/Defaults/Defaults.swift b/Sources/Defaults/Defaults.swift index 0d41884..bb0cf88 100644 --- a/Sources/Defaults/Defaults.swift +++ b/Sources/Defaults/Defaults.swift @@ -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: 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: Keys { public let name: String @@ -103,15 +98,7 @@ public final class Defaults { } } - /// Access a defaults value using a `Defaults.OptionalKey`. - public static subscript(key: OptionalKey) -> 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(key: NSSecureCodingOptionalKey) -> 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("unicorn") - } - - Defaults[.unicorn] = "🦄" - - Defaults.reset(.unicorn) - - Defaults[.unicorn] - //=> nil - ``` - */ - public static func reset(_ keys: OptionalKey..., 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(_ keys: NSSecureCodingOptionalKey..., 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("unicorn") - } - - Defaults[.unicorn] = "🦄" - - Defaults.reset(.unicorn) - - Defaults[.unicorn] - //=> nil - ``` - */ - public static func reset(_ keys: [OptionalKey], 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(_ key: String) -> Value? { if UserDefaults.isNativelySupportedType(Value.self) { @@ -334,6 +286,11 @@ extension UserDefaults { } private func _set(_ 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(_ 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(key: Defaults.OptionalKey) -> 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(key: Defaults.NSSecureCodingOptionalKey) -> Value? { get { _get(key.name) } @@ -392,16 +338,23 @@ extension UserDefaults { } } - fileprivate static func isNativelySupportedType(_ type: Value.Type) -> Bool { + fileprivate static func isNativelySupportedType(_ 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 diff --git a/Sources/Defaults/Observation+Combine.swift b/Sources/Defaults/Observation+Combine.swift index bd27d73..c854e50 100644 --- a/Sources/Defaults/Observation+Combine.swift +++ b/Sources/Defaults/Observation+Combine.swift @@ -60,7 +60,7 @@ extension Defaults { self.options = options } - func receive(subscriber: S) where S : Subscriber, DefaultsPublisher.Failure == S.Failure, DefaultsPublisher.Output == S.Input { + func receive(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("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( - _ key: Defaults.OptionalKey, - options: NSKeyValueObservingOptions = [.initial, .old, .new] - ) -> AnyPublisher, Never> { - let publisher = DefaultsPublisher(suite: key.suite, key: key.name, options: options) - .map { OptionalKeyChange(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` 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( - keys: Defaults.OptionalKey..., - options: NSKeyValueObservingOptions = [.initial, .old, .new] - ) -> AnyPublisher { - let initial = Empty(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 diff --git a/Sources/Defaults/Observation.swift b/Sources/Defaults/Observation.swift index ec5b4c5..38c1983 100644 --- a/Sources/Defaults/Observation.swift +++ b/Sources/Defaults/Observation.swift @@ -113,22 +113,6 @@ extension Defaults { } } - public struct OptionalKeyChange { - 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 { 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("isUnicornMode") - } - - let observer = Defaults.observe(.isUnicornMode) { change in - print(change.newValue) - //=> Optional(nil) - } - ``` - */ - public static func observe( - _ key: Defaults.OptionalKey, - options: NSKeyValueObservingOptions = [.initial, .old, .new], - handler: @escaping (OptionalKeyChange) -> Void - ) -> DefaultsObservation { - let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in - handler( - OptionalKeyChange(change: change) - ) - } - observation.start(options: options) - return observation - } - /** Observe an optional defaults key. */ diff --git a/Sources/Defaults/util.swift b/Sources/Defaults/util.swift index 714237b..272a559 100644 --- a/Sources/Defaults/util.swift +++ b/Sources/Defaults/util.swift @@ -18,16 +18,19 @@ extension Decodable { } } -final class AssociatedObject { - subscript(index: Any) -> T? { + +final class ObjectAssociation { + 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(_ type: T.Type) -> Bool { + type is _DefaultsOptionalType.Type +} diff --git a/Tests/DefaultsTests/DefaultsTests.swift b/Tests/DefaultsTests/DefaultsTests.swift index 2f2e39a..6cebcf4 100644 --- a/Tests/DefaultsTests/DefaultsTests.swift +++ b/Tests/DefaultsTests/DefaultsTests.swift @@ -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("independentOptionalKey") + let key = Defaults.Key("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("observeOptionalKey") + let key = Defaults.Key("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("observeOptionalKey1") - let key2 = Defaults.OptionalKey("observeOptionalKey2") + let key1 = Defaults.Key("observeOptionalKey1") + let key2 = Defaults.Key("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("observeOptionalKey") + let key = Defaults.Key("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("optionalKey1") - let key2 = Defaults.OptionalKey("optionalKey2") + let key1 = Defaults.Key("optionalKey1") + let key2 = Defaults.Key("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("aoptionalKey1") - let key2 = Defaults.OptionalKey("aoptionalKey2") - let key3 = Defaults.OptionalKey("aoptionalKey3") + let key1 = Defaults.Key("aoptionalKey1") + let key2 = Defaults.Key("aoptionalKey2") + let key3 = Defaults.Key("aoptionalKey3") Defaults[key1] = newString1 Defaults[key2] = newString2 Defaults[key3] = newString3 diff --git a/readme.md b/readme.md index 5b91a94..e158a8a 100644 --- a/readme.md +++ b/readme.md @@ -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("name") + static let name = Key("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(_ 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(_ keys: Defaults.Key..., suite: UserDefaults = .standard) 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) @@ -364,14 +352,6 @@ Defaults.observe( ) -> DefaultsObservation ``` -```swift -Defaults.observe( - _ key: Defaults.OptionalKey, - options: NSKeyValueObservingOptions = [.initial, .old, .new], - handler: @escaping (OptionalKeyChange) -> Void -) -> DefaultsObservation -``` - ```swift Defaults.observe( _ key: Defaults.NSSecureCodingOptionalKey, @@ -402,13 +382,6 @@ Defaults.publisher( ) -> AnyPublisher, Never> ``` -```swift -Defaults.publisher( - _ key: Defaults.OptionalKey, - options: NSKeyValueObservingOptions = [.initial, .old, .new] -) -> AnyPublisher, Never> -``` - ```swift Defaults.publisher( _ key: Defaults.NSSecureCodingOptionalKey, @@ -438,13 +411,6 @@ Defaults.publisher( ) -> AnyPublisher { ``` -```swift -Defaults.publisher( - keys: Defaults.OptionalKey..., - options: NSKeyValueObservingOptions = [.initial, .old, .new] -) -> AnyPublisher { -``` - ```swift Defaults.publisher( keys: Defaults.NSSecureCodingOptionalKey...,