diff --git a/Sources/Defaults/Defaults.swift b/Sources/Defaults/Defaults.swift index eca307c..07cbc31 100644 --- a/Sources/Defaults/Defaults.swift +++ b/Sources/Defaults/Defaults.swift @@ -41,7 +41,15 @@ public enum Defaults { } public final class Key: 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. @@ -51,7 +59,7 @@ public enum Defaults { The `default` parameter should not be used if the `Value` type is an optional. */ public init(_ key: String, default defaultValue: Value, suite: UserDefaults = .standard) { - self.defaultValue = defaultValue + self.defaultValueGetter = { defaultValue } 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. 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("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(key: Key) -> Value { diff --git a/Tests/DefaultsTests/DefaultsTests.swift b/Tests/DefaultsTests/DefaultsTests.swift index 57ffe6e..5e9af35 100644 --- a/Tests/DefaultsTests/DefaultsTests.swift +++ b/Tests/DefaultsTests/DefaultsTests.swift @@ -15,6 +15,8 @@ extension Defaults.Keys { static let data = Key("data", default: Data([])) static let date = Key("date", default: fixtureDate) static let uuid = Key("uuid") + static let defaultDynamicDate = Key("defaultDynamicOptionalDate") { Date(timeIntervalSince1970: 0) } + static let defaultDynamicOptionalDate = Key("defaultDynamicOptionalDate") { Date(timeIntervalSince1970: 1) } } final class DefaultsTests: XCTestCase { @@ -54,6 +56,17 @@ final class DefaultsTests: XCTestCase { XCTAssertEqual(Defaults[url], fixtureURL2) } + func testInitializeDynamicDateKey() { + _ = Defaults.Key("independentInitializeDynamicDateKey") { + XCTFail("Init dynamic key should not trigger getter") + return Date() + } + _ = Defaults.Key("independentInitializeDynamicOptionalDateKey") { + XCTFail("Init dynamic optional key should not trigger getter") + return Date() + } + } + func testKeyRegistersDefault() { let keyName = "registersDefault" XCTAssertFalse(UserDefaults.standard.bool(forKey: keyName)) @@ -100,6 +113,27 @@ final class DefaultsTests: XCTestCase { 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.defaultDynamicDate.name) as! Date, next) + Defaults.Key.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.defaultDynamicOptionalDate.name) as! Date, next) + Defaults[.defaultDynamicOptionalDate] = nil + XCTAssertEqual(Defaults[.defaultDynamicOptionalDate], Date(timeIntervalSince1970: 1)) + XCTAssertNil(UserDefaults.standard.object(forKey: Defaults.Key.defaultDynamicOptionalDate.name)) + } + func testFileURLType() { XCTAssertEqual(Defaults[.file], fixtureFileURL) } @@ -188,6 +222,38 @@ final class DefaultsTests: XCTestCase { 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("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, *) func testObserveMultipleKeysCombine() { let key1 = Defaults.Key("observeKey1", default: "x") @@ -321,6 +387,26 @@ final class DefaultsTests: XCTestCase { waitForExpectations(timeout: 10) } + func testObserveDynamicOptionalDateKey() { + let first = Date(timeIntervalSince1970: 0) + let second = Date(timeIntervalSince1970: 1) + let key = Defaults.Key("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() { let key1 = Defaults.Key("preventPropagation0", default: nil) let expect = expectation(description: "No infinite recursion") diff --git a/readme.md b/readme.md index d8f75e4..6bd48f9 100644 --- a/readme.md +++ b/readme.md @@ -123,6 +123,14 @@ if let name = Defaults[.name] { 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("camera") { .default(for: .video) } +} +``` + --- ### Enum example @@ -383,6 +391,9 @@ print(UserDefaults.standard.bool(forKey: Defaults.Keys.isUnicornMode.name)) //=> true ``` +> **Note** +> A `Defaults.Key` with a dynamic default value will not register the default value in `UserDefaults`. + ## API ### `Defaults`