Support dynamic default value (#121)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
This commit is contained in:
parent
1b1a057220
commit
fbc67fd179
|
@ -41,7 +41,15 @@ public enum Defaults {
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class Key<Value: Serializable>: AnyKey {
|
public final class Key<Value: Serializable>: AnyKey {
|
||||||
public let defaultValue: Value
|
/**
|
||||||
|
It will be executed in these situations:
|
||||||
|
|
||||||
|
- `UserDefaults.object(forKey: string)` returns `nil`
|
||||||
|
- A `bridge` cannot deserialize `Value` from `UserDefaults`
|
||||||
|
*/
|
||||||
|
private let defaultValueGetter: () -> Value
|
||||||
|
|
||||||
|
public var defaultValue: Value { defaultValueGetter() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Create a defaults key.
|
Create a defaults key.
|
||||||
|
@ -51,7 +59,7 @@ public enum Defaults {
|
||||||
The `default` parameter should not be used if the `Value` type is an optional.
|
The `default` parameter should not be used if the `Value` type is an optional.
|
||||||
*/
|
*/
|
||||||
public init(_ key: String, default defaultValue: Value, suite: UserDefaults = .standard) {
|
public init(_ key: String, default defaultValue: Value, suite: UserDefaults = .standard) {
|
||||||
self.defaultValue = defaultValue
|
self.defaultValueGetter = { defaultValue }
|
||||||
|
|
||||||
super.init(name: key, suite: suite)
|
super.init(name: key, suite: suite)
|
||||||
|
|
||||||
|
@ -66,6 +74,25 @@ public enum Defaults {
|
||||||
// Sets the default value in the actual UserDefaults, so it can be used in other contexts, like binding.
|
// Sets the default value in the actual UserDefaults, so it can be used in other contexts, like binding.
|
||||||
suite.register(defaults: [name: serialized])
|
suite.register(defaults: [name: serialized])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Create a defaults key with a dynamic default value.
|
||||||
|
|
||||||
|
This can be useful in cases where you cannot define a static default value as it may change during the lifetime of the app.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
extension Defaults.Keys {
|
||||||
|
static let camera = Key<AVCaptureDevice?>("camera") { .default(for: .video) }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Note: This initializer will not set the default value in the actual `UserDefaults`. This should not matter much though. It's only really useful if you use legacy KVO bindings.
|
||||||
|
*/
|
||||||
|
public init(_ key: String, suite: UserDefaults = .standard, default defaultValueGetter: @escaping () -> Value) {
|
||||||
|
self.defaultValueGetter = defaultValueGetter
|
||||||
|
|
||||||
|
super.init(name: key, suite: suite)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static subscript<Value: Serializable>(key: Key<Value>) -> Value {
|
public static subscript<Value: Serializable>(key: Key<Value>) -> Value {
|
||||||
|
|
|
@ -15,6 +15,8 @@ extension Defaults.Keys {
|
||||||
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)
|
||||||
static let uuid = Key<UUID?>("uuid")
|
static let uuid = Key<UUID?>("uuid")
|
||||||
|
static let defaultDynamicDate = Key<Date>("defaultDynamicOptionalDate") { Date(timeIntervalSince1970: 0) }
|
||||||
|
static let defaultDynamicOptionalDate = Key<Date?>("defaultDynamicOptionalDate") { Date(timeIntervalSince1970: 1) }
|
||||||
}
|
}
|
||||||
|
|
||||||
final class DefaultsTests: XCTestCase {
|
final class DefaultsTests: XCTestCase {
|
||||||
|
@ -54,6 +56,17 @@ final class DefaultsTests: XCTestCase {
|
||||||
XCTAssertEqual(Defaults[url], fixtureURL2)
|
XCTAssertEqual(Defaults[url], fixtureURL2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testInitializeDynamicDateKey() {
|
||||||
|
_ = Defaults.Key<Date>("independentInitializeDynamicDateKey") {
|
||||||
|
XCTFail("Init dynamic key should not trigger getter")
|
||||||
|
return Date()
|
||||||
|
}
|
||||||
|
_ = Defaults.Key<Date?>("independentInitializeDynamicOptionalDateKey") {
|
||||||
|
XCTFail("Init dynamic optional key should not trigger getter")
|
||||||
|
return Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testKeyRegistersDefault() {
|
func testKeyRegistersDefault() {
|
||||||
let keyName = "registersDefault"
|
let keyName = "registersDefault"
|
||||||
XCTAssertFalse(UserDefaults.standard.bool(forKey: keyName))
|
XCTAssertFalse(UserDefaults.standard.bool(forKey: keyName))
|
||||||
|
@ -100,6 +113,27 @@ final class DefaultsTests: XCTestCase {
|
||||||
XCTAssertEqual(Defaults[.date], newDate)
|
XCTAssertEqual(Defaults[.date], newDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testDynamicDateType() {
|
||||||
|
XCTAssertEqual(Defaults[.defaultDynamicDate], Date(timeIntervalSince1970: 0))
|
||||||
|
let next = Date(timeIntervalSince1970: 1)
|
||||||
|
Defaults[.defaultDynamicDate] = next
|
||||||
|
XCTAssertEqual(Defaults[.defaultDynamicDate], next)
|
||||||
|
XCTAssertEqual(UserDefaults.standard.object(forKey: Defaults.Key<Date>.defaultDynamicDate.name) as! Date, next)
|
||||||
|
Defaults.Key<Date>.defaultDynamicDate.reset()
|
||||||
|
XCTAssertEqual(Defaults[.defaultDynamicDate], Date(timeIntervalSince1970: 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDynamicOptionalDateType() {
|
||||||
|
XCTAssertEqual(Defaults[.defaultDynamicOptionalDate], Date(timeIntervalSince1970: 1))
|
||||||
|
let next = Date(timeIntervalSince1970: 2)
|
||||||
|
Defaults[.defaultDynamicOptionalDate] = next
|
||||||
|
XCTAssertEqual(Defaults[.defaultDynamicOptionalDate], next)
|
||||||
|
XCTAssertEqual(UserDefaults.standard.object(forKey: Defaults.Key<Date>.defaultDynamicOptionalDate.name) as! Date, next)
|
||||||
|
Defaults[.defaultDynamicOptionalDate] = nil
|
||||||
|
XCTAssertEqual(Defaults[.defaultDynamicOptionalDate], Date(timeIntervalSince1970: 1))
|
||||||
|
XCTAssertNil(UserDefaults.standard.object(forKey: Defaults.Key<Date>.defaultDynamicOptionalDate.name))
|
||||||
|
}
|
||||||
|
|
||||||
func testFileURLType() {
|
func testFileURLType() {
|
||||||
XCTAssertEqual(Defaults[.file], fixtureFileURL)
|
XCTAssertEqual(Defaults[.file], fixtureFileURL)
|
||||||
}
|
}
|
||||||
|
@ -188,6 +222,38 @@ final class DefaultsTests: XCTestCase {
|
||||||
waitForExpectations(timeout: 10)
|
waitForExpectations(timeout: 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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 testDynamicOptionalDateTypeCombine() {
|
||||||
|
let first = Date(timeIntervalSince1970: 0)
|
||||||
|
let second = Date(timeIntervalSince1970: 1)
|
||||||
|
let third = Date(timeIntervalSince1970: 2)
|
||||||
|
let key = Defaults.Key<Date?>("combineDynamicOptionalDateKey") { first }
|
||||||
|
let expect = expectation(description: "Observation closure being called")
|
||||||
|
|
||||||
|
let publisher = Defaults
|
||||||
|
.publisher(key, options: [])
|
||||||
|
.map { ($0.oldValue, $0.newValue) }
|
||||||
|
.collect(3)
|
||||||
|
|
||||||
|
let expectedValues: [(Date?, Date?)] = [(first, second), (second, third), (third, first)]
|
||||||
|
|
||||||
|
let cancellable = publisher.sink { actualValues in
|
||||||
|
for (expected, actual) in zip(expectedValues, actualValues) {
|
||||||
|
XCTAssertEqual(expected.0, actual.0)
|
||||||
|
XCTAssertEqual(expected.1, actual.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
Defaults[key] = second
|
||||||
|
Defaults[key] = third
|
||||||
|
Defaults.reset(key)
|
||||||
|
cancellable.cancel()
|
||||||
|
|
||||||
|
waitForExpectations(timeout: 10)
|
||||||
|
}
|
||||||
|
|
||||||
@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, *)
|
@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 testObserveMultipleKeysCombine() {
|
func testObserveMultipleKeysCombine() {
|
||||||
let key1 = Defaults.Key<String>("observeKey1", default: "x")
|
let key1 = Defaults.Key<String>("observeKey1", default: "x")
|
||||||
|
@ -321,6 +387,26 @@ final class DefaultsTests: XCTestCase {
|
||||||
waitForExpectations(timeout: 10)
|
waitForExpectations(timeout: 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testObserveDynamicOptionalDateKey() {
|
||||||
|
let first = Date(timeIntervalSince1970: 0)
|
||||||
|
let second = Date(timeIntervalSince1970: 1)
|
||||||
|
let key = Defaults.Key<Date?>("observeDynamicOptionalDate") { first }
|
||||||
|
|
||||||
|
let expect = expectation(description: "Observation closure being called")
|
||||||
|
|
||||||
|
var observation: Defaults.Observation!
|
||||||
|
observation = Defaults.observe(key, options: []) { change in
|
||||||
|
XCTAssertEqual(change.oldValue, first)
|
||||||
|
XCTAssertEqual(change.newValue, second)
|
||||||
|
observation.invalidate()
|
||||||
|
expect.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
Defaults[key] = second
|
||||||
|
|
||||||
|
waitForExpectations(timeout: 10)
|
||||||
|
}
|
||||||
|
|
||||||
func testObservePreventPropagation() {
|
func testObservePreventPropagation() {
|
||||||
let key1 = Defaults.Key<Bool?>("preventPropagation0", default: nil)
|
let key1 = Defaults.Key<Bool?>("preventPropagation0", default: nil)
|
||||||
let expect = expectation(description: "No infinite recursion")
|
let expect = expectation(description: "No infinite recursion")
|
||||||
|
|
11
readme.md
11
readme.md
|
@ -123,6 +123,14 @@ if let name = Defaults[.name] {
|
||||||
|
|
||||||
The default value is then `nil`.
|
The default value is then `nil`.
|
||||||
|
|
||||||
|
You can also specify a dynamic default value. This can be useful when the default value may change during the lifetime of the app:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
extension Defaults.Keys {
|
||||||
|
static let camera = Key<AVCaptureDevice?>("camera") { .default(for: .video) }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Enum example
|
### Enum example
|
||||||
|
@ -383,6 +391,9 @@ print(UserDefaults.standard.bool(forKey: Defaults.Keys.isUnicornMode.name))
|
||||||
//=> true
|
//=> true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
> A `Defaults.Key` with a dynamic default value will not register the default value in `UserDefaults`.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
### `Defaults`
|
### `Defaults`
|
||||||
|
|
Loading…
Reference in New Issue