Skip to content

Commit

Permalink
@UserDefaultsBacked: Added computed-only init without default
Browse files Browse the repository at this point in the history
  • Loading branch information
orchetect committed Jan 13, 2023
1 parent 6679b6c commit 431d8ce
Show file tree
Hide file tree
Showing 2 changed files with 261 additions and 20 deletions.
130 changes: 110 additions & 20 deletions Sources/OTCore/Extensions/Foundation/UserDefaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ extension UserDefaults {
/// **OTCore:**
/// Read and write the value of a `UserDefaults` key.
///
/// If a default value is provided, the `Value` will be treated as a non-Optional.
/// If a defaults suite is not specified, `.standard` will be used.
///
/// If a default value is provided, the `Value` will be treated as a non-Optional with a default.
///
/// @UserDefaultsBacked(key: "myPref")
/// var myPref = true
/// var myPref: Bool = true
///
/// If no default is provided, the `Value` will be treated as an Optional.
///
Expand All @@ -72,27 +74,74 @@ extension UserDefaults {
/// Both `get` and `set` closures allow for custom transform code.
/// If either closure returns nil, the default value of 1 will be used.
///
/// // UserDefaults will store this as a `String`, but the var is an `Int`.
/// @UserDefaultsBacked(key: "myPref", get: { Int($0) }, set: { "\($0)" })
/// // Stored as a `String`, but the var is an `Int`.
/// // get closure: transform `String` into `Int`
/// // set closure: transform `Int` into `String`
/// @UserDefaultsBacked(
/// key: "myPref",
/// get: { Int($0) },
/// set: { "\($0)" }
/// )
/// var myPref: Int = 1
///
/// If a defaults suite is not specified, `.standard` will be used.
/// A non-defaulted declaration relies on the closures to process the values with no default.
///
/// // Stored as a `String`, but the var is an `Int`.
/// // get closure: transform `String?` into `Int`
/// // set closure: transform `Int` into `String`
/// @UserDefaultsBacked(
/// key: "myPref",
/// get: { Int($0 ?? "") ?? 0 },
/// set: { "\($0)" }
/// )
/// var myPref: Int
///
/// Additional conveniences are available through specific parameters.
///
/// A special value validation closure is available when the value type matches the stored value
/// type.
///
/// @UserDefaultsBacked(
/// key: "myPref",
/// validation: { $0.trimmingCharacters(in: .whitespaces) },
/// )
/// var pref = " test " // will be stored as "test"
///
/// A special value clamping closure is available when the value type matches the stored value
/// type. Any types (not just integers) that can form a range can be clamped.
///
/// @UserDefaultsBacked(key: "myPref", clamped: 5 ... 10)
/// var pref = 1 // will be clamped to 5
///
@propertyWrapper
public struct UserDefaultsBacked<Value, StorageValue> {
private let key: String
private let defaultValue: Value
private let defaultValue: Any
public var storage: UserDefaults

private let getTransformation: ((_ storedValue: StorageValue) -> Value?)
private let setTransformation: ((_ newValue: Value) -> StorageValue?)

private let computedOnly: Bool
private let getTransformationComputedOnly: ((_ storedValue: StorageValue?) -> Value)
private let setTransformationComputedOnly: ((_ newValue: Value) -> StorageValue)

// note: "defaultValue as! Value" is guaranteed to work because it's only used
// where the value is known to be of type Value.
// it's an unfortunate workaround that defaultValue is Any but it allows us to
// build this big beautiful single propertyWrapper with multiple uses
// instead of having to split it up into multiple different structs.
public var wrappedValue: Value {
get {
guard let value = storage.value(forKey: key) as? StorageValue else {
return defaultValue
let value = storage.value(forKey: key) as? StorageValue
if computedOnly {
return getTransformationComputedOnly(value)
}
let processed = process(value)
return processed ?? defaultValue
guard let value = value else {
return defaultValue as! Value
}
let processed = getTransformation(value)
return processed ?? defaultValue as! Value
}
set {
if let asOptional = newValue as? OTCoreOptional {
Expand All @@ -101,24 +150,27 @@ public struct UserDefaultsBacked<Value, StorageValue> {
// otherwise .setValue() will throw an exception
storage.removeObject(forKey: key)
} else if let unwrappedNewValue = asOptional.asAny() as? Value {
let processedValue = process(unwrappedNewValue)
var processedValue: StorageValue?
if computedOnly {
processedValue = setTransformationComputedOnly(unwrappedNewValue)
} else {
processedValue = setTransformation(unwrappedNewValue)
}
storage.setValue(processedValue, forKey: key)
}
} else {
let processedValue = process(newValue) ?? process(defaultValue)
var processedValue: StorageValue?
if computedOnly {
processedValue = setTransformationComputedOnly(newValue)
} else {
processedValue = setTransformation(newValue)
?? setTransformation(defaultValue as! Value)
}
storage.setValue(processedValue, forKey: key)
}
}
}

private func process(_ value: StorageValue) -> Value? {
getTransformation(value)
}

private func process(_ value: Value) -> StorageValue? {
setTransformation(value)
}

// MARK: Init - Same Type

public init(
Expand All @@ -133,6 +185,9 @@ public struct UserDefaultsBacked<Value, StorageValue> {
// not used
getTransformation = { $0 }
setTransformation = { $0 }
computedOnly = false
getTransformationComputedOnly = { _ in defaultValue }
setTransformationComputedOnly = { $0 }

// update stored value
let readValue = wrappedValue
Expand Down Expand Up @@ -164,6 +219,11 @@ public struct UserDefaultsBacked<Value, StorageValue> {
// clamp initial value
self.defaultValue = closure(defaultValue)

// not used
computedOnly = false
getTransformationComputedOnly = { _ in defaultValue }
setTransformationComputedOnly = { $0 }

// update stored value
let readValue = wrappedValue
wrappedValue = readValue
Expand All @@ -181,6 +241,9 @@ public struct UserDefaultsBacked<Value, StorageValue> {
// not used
getTransformation = closure
setTransformation = closure
computedOnly = false
getTransformationComputedOnly = { _ in defaultValue }
setTransformationComputedOnly = { $0 }

// validate initial value
self.defaultValue = closure(defaultValue)
Expand All @@ -207,10 +270,37 @@ public struct UserDefaultsBacked<Value, StorageValue> {
self.setTransformation = setTransformation
self.defaultValue = defaultValue

// not used
computedOnly = false
getTransformationComputedOnly = { _ in defaultValue }
setTransformationComputedOnly = { setTransformation($0)! }

// update stored value
let readValue = wrappedValue
wrappedValue = readValue
}

/// Uses get and set transform closures to allow a value to have a different underlying storage
/// type.
public init(
key: String,
get getTransformation: @escaping (_ storedValue: StorageValue?) -> Value,
set setTransformation: @escaping (_ newValue: Value) -> StorageValue,
storage: UserDefaults = .standard
) {
computedOnly = true

self.key = key
self.storage = storage
self.getTransformationComputedOnly = getTransformation
self.setTransformationComputedOnly = setTransformation

// not used
self.getTransformation = { _ in nil }
self.setTransformation = { _ in nil }
// safe because we ensure to not use this property when computedOnly == true
self.defaultValue = Void.self
}
}

extension UserDefaultsBacked where Value: ExpressibleByNilLiteral {
Expand Down
151 changes: 151 additions & 0 deletions Tests/OTCoreTests/Extensions/Foundation/UserDefaults Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,157 @@ class Extensions_Foundation_UserDefaults_Tests: XCTestCase {
XCTAssertEqual(ud.integer(forKey: DummyPrefs.prefKey), 4)
XCTAssertEqual(dummyPrefs.pref, 4)
}

func testUserDefaultsBacked_ComputedOnly_Generic() {
struct DummyPrefs {
static let prefKey = "computedOnlyPref"

// not used, just here to see if this alternate syntax compiles
@UserDefaultsBacked<Int, String>(
key: prefKey,
get: { $0 != nil ? Int($0!) ?? 0 : 0 },
set: { "\($0)" },
storage: ud
)
private var pref1: Int

// not used, just here to see if this alternate syntax compiles
@UserDefaultsBacked(
key: prefKey,
get: { (storedValue: String?) in
storedValue != nil ? Int(storedValue!) ?? 0 : 0
},
set: { (newValue: Int) -> String in "\(newValue)" },
storage: ud
)
private var pref2: Int
}

_ = DummyPrefs() // forces property wrappers to init
}

func testUserDefaultsBacked_ComputedOnly_NoPreviousValue() {
struct DummyPrefs {
static let prefKey = "computedOnlyPref"

@UserDefaultsBacked(
key: prefKey,
get: { $0 != nil ? Int($0!) ?? -1 : -1 },
set: { "\($0)" },
storage: ud
)
var pref: Int
}

var dummyPrefs = DummyPrefs()

// default value
XCTAssertEqual(ud.integerOptional(forKey: DummyPrefs.prefKey), nil)
XCTAssertEqual(dummyPrefs.pref, -1)

dummyPrefs.pref = 4

XCTAssertEqual(ud.integerOptional(forKey: DummyPrefs.prefKey), 4)
XCTAssertEqual(dummyPrefs.pref, 4)
}

func testUserDefaultsBacked_ComputedOnly_Optional_NoPreviousValue() {
struct DummyPrefs {
static let prefKey = "computedOnlyOptionalPref"

@UserDefaultsBacked(
key: prefKey,
get: { $0 != nil ? Int($0!) ?? -1 : -1 },
set: { "\($0 ?? -1)" },
storage: ud
)
var pref: Int?
}

var dummyPrefs = DummyPrefs()

// default value
XCTAssertEqual(ud.integerOptional(forKey: DummyPrefs.prefKey), nil)
XCTAssertEqual(dummyPrefs.pref, -1)

dummyPrefs.pref = 4

XCTAssertEqual(ud.integerOptional(forKey: DummyPrefs.prefKey), 4)
XCTAssertEqual(dummyPrefs.pref, 4)

dummyPrefs.pref = nil

XCTAssertEqual(ud.integerOptional(forKey: DummyPrefs.prefKey), nil)
XCTAssertEqual(dummyPrefs.pref, -1)
}

func testUserDefaultsBacked_ComputedOnly_HasPreviousValue() {
struct DummyPrefs {
static let prefKey = "computedOnlyPref"

@UserDefaultsBacked(
key: prefKey,
get: { $0 != nil ? Int($0!) ?? -1 : -1 },
set: { "\($0)" },
storage: ud
)
var pref: Int
}

var dummyPrefs = DummyPrefs()

// set a pre-existing value
ud.set("6", forKey: DummyPrefs.prefKey)

// default value
XCTAssertEqual(ud.integerOptional(forKey: DummyPrefs.prefKey), 6)
XCTAssertEqual(dummyPrefs.pref, 6)

dummyPrefs.pref = 4

XCTAssertEqual(ud.integerOptional(forKey: DummyPrefs.prefKey), 4)
XCTAssertEqual(dummyPrefs.pref, 4)
}

func testUserDefaultsBacked_ComputedOnly_Optional_HasPreviousValue() {
struct DummyPrefs {
static let prefKey = "computedOnlyOptionalPref"

@UserDefaultsBacked(
key: prefKey,
get: { $0 != nil ? Int($0!) ?? -1 : -1 },
set: { "\($0 ?? -1)" },
storage: ud
)
var pref: Int?

@UserDefaultsBacked(
key: "myPref",
get: { Int($0 ?? "") ?? 0 },
set: { "\($0)" }
)
var myPref: Int
}

var dummyPrefs = DummyPrefs()

// set a pre-existing value
ud.set("6", forKey: DummyPrefs.prefKey)

// default value
XCTAssertEqual(ud.integerOptional(forKey: DummyPrefs.prefKey), 6)
XCTAssertEqual(dummyPrefs.pref, 6)

dummyPrefs.pref = 4

XCTAssertEqual(ud.integerOptional(forKey: DummyPrefs.prefKey), 4)
XCTAssertEqual(dummyPrefs.pref, 4)

dummyPrefs.pref = nil

XCTAssertEqual(ud.integerOptional(forKey: DummyPrefs.prefKey), nil)
XCTAssertEqual(dummyPrefs.pref, -1)
}
}

#endif

0 comments on commit 431d8ce

Please sign in to comment.