Skip to content

Commit

Permalink
@UserDefaultsStorage: Added ability to use any Codable type
Browse files Browse the repository at this point in the history
  • Loading branch information
orchetect committed Oct 2, 2023
1 parent 611556a commit 98217c7
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 1 deletion.
51 changes: 50 additions & 1 deletion Sources/OTCore/Extensions/Foundation/UserDefaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ extension UserDefaults {
}
}

// MARK: - Backed PropertyWrappers
// MARK: - UserDefaults Storage PropertyWrappers

/// **OTCore:**
/// Read and write the value of a `UserDefaults` key.
Expand Down Expand Up @@ -265,6 +265,40 @@ public struct UserDefaultsStorage<Value, StorageValue> where StorageValue: UserD
wrappedValue = readValue
}

// MARK: Init - Same Type Codable

/// Store and retrieve any object conforming to `Codable` by using JSON serialization and storing as `String` in UserDefaults.
@_disfavoredOverload
public init(
wrappedValue defaultValue: Value,
key: String,
storage: UserDefaults = .standard
) where Value: Codable, StorageValue == String {
self.defaultValue = defaultValue
self.key = key
self.storage = storage

getTransformation = { storedValue in
let decoder = JSONDecoder()
guard let data = storedValue.data(using: .utf8),
let decoded = try? decoder.decode(Value.self, from: data)
else { return nil }
return decoded
}
setTransformation = { newValue in
let encoder = JSONEncoder()
guard let encoded = try? encoder.encode(newValue),
let string = encoded.toString(using: .utf8)
else { return nil }
return string
}

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

// MARK: - Different Types

/// Uses get and set transform closures to allow a value to have a different underlying storage
Expand Down Expand Up @@ -342,6 +376,21 @@ extension UserDefaultsStorage where Value: ExpressibleByNilLiteral {
)
}

// MARK: Init - Same Type Codable

/// Store and retrieve any object conforming to `Codable` by using JSON serialization and storing as `String` in UserDefaults.
@_disfavoredOverload
public init(
key: String,
storage: UserDefaults = .standard
) where Value: Codable, StorageValue == String {
self.init(
wrappedValue: nil,
key: key,
storage: storage
)
}

// MARK: - Different Types

/// Uses get and set transform closures to allow a value to have a different underlying storage
Expand Down
129 changes: 129 additions & 0 deletions Tests/OTCoreTests/Extensions/Foundation/UserDefaults Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,135 @@ class Extensions_Foundation_UserDefaults_Tests: XCTestCase {
XCTAssertEqual(ud.float(forKey: DummyPrefs.prefKey), 5.0)
XCTAssertEqual(dummyPrefs.pref, 5.0)
}

// MARK: Codable

func testUserDefaultsStorage_Codable_NonDefaulted_Optional_NoPreviousValue() {
struct Prefs: Codable, Equatable {
var someString: String
var someInt: Int
var array: [String]
var dict: [String: String]
var nestedStruct: NestedStruct

struct NestedStruct: Codable, Equatable {
var nestedInt: Int
var nestedString: String
}
}

struct DummyPrefs {
static let prefKey = "codablePref"
static let prefsTemplate = Prefs(
someString: "hello",
someInt: 123,
array: ["one", "two"],
dict: ["Key1": "ValA", "Key2": "ValB"],
nestedStruct: .init(nestedInt: 456, nestedString: "test")
)

@UserDefaultsStorage(key: prefKey, storage: ud)
var pref: Prefs?
}

var dummyPrefs = DummyPrefs()

// default value
XCTAssertNil(ud.string(forKey: DummyPrefs.prefKey))
XCTAssertNil(dummyPrefs.pref)

dummyPrefs.pref = DummyPrefs.prefsTemplate

XCTAssertNotNil(ud.string(forKey: DummyPrefs.prefKey))
XCTAssertEqual(dummyPrefs.pref, DummyPrefs.prefsTemplate)

dummyPrefs.pref = nil

XCTAssertNil(ud.string(forKey: DummyPrefs.prefKey))
XCTAssertNil(dummyPrefs.pref)
}

func testUserDefaultsStorage_Codable_Defaulted_NoPreviousValue() {
struct Prefs: Codable, Equatable {
var someString: String
var someInt: Int
var array: [String]
var dict: [String: String]
var nestedStruct: NestedStruct

struct NestedStruct: Codable, Equatable {
var nestedInt: Int
var nestedString: String
}
}

struct DummyPrefs {
static let prefKey = "codablePref"
static let prefsTemplate = Prefs(
someString: "hello",
someInt: 123,
array: ["one", "two"],
dict: ["Key1": "ValA", "Key2": "ValB"],
nestedStruct: .init(nestedInt: 456, nestedString: "test")
)

@UserDefaultsStorage(key: prefKey, storage: ud)
var pref: Prefs = prefsTemplate
}

var dummyPrefs = DummyPrefs()

// default value
XCTAssertNil(ud.string(forKey: DummyPrefs.prefKey))
XCTAssertEqual(dummyPrefs.pref, DummyPrefs.prefsTemplate)

dummyPrefs.pref = DummyPrefs.prefsTemplate

XCTAssertNotNil(ud.string(forKey: DummyPrefs.prefKey))
XCTAssertEqual(dummyPrefs.pref, DummyPrefs.prefsTemplate)
}

/// Test that a primitive type that already conforms to Codable is stored normally and
/// not by using the Codable inits on `@UserDefaultsStorage`.
func testUserDefaultsStorage_String_NonCodable() {
struct DummyPrefs {
static let prefKey = "stringPref"

@UserDefaultsStorage(key: prefKey, storage: ud)
var pref: String = "A"
}

var dummyPrefs = DummyPrefs()

XCTAssertEqual(ud.string(forKey: DummyPrefs.prefKey), "A")
XCTAssertEqual(dummyPrefs.pref, "A")

dummyPrefs.pref = "ABC"

XCTAssertEqual(ud.string(forKey: DummyPrefs.prefKey), "ABC")
XCTAssertEqual(dummyPrefs.pref, "ABC")
}

/// Test that a primitive type that already conforms to Codable is stored normally and
/// not by using the Codable inits on `@UserDefaultsStorage`.
func testUserDefaultsStorage_String_Optional_NonCodable() {
struct DummyPrefs {
static let prefKey = "stringPref"

@UserDefaultsStorage(key: prefKey, storage: ud)
var pref: String?
}

var dummyPrefs = DummyPrefs()

XCTAssertNil(ud.string(forKey: DummyPrefs.prefKey))
XCTAssertNil(dummyPrefs.pref)

dummyPrefs.pref = "ABC"

XCTAssertEqual(ud.string(forKey: DummyPrefs.prefKey), "ABC")
XCTAssertEqual(dummyPrefs.pref, "ABC")
}
}

#endif

0 comments on commit 98217c7

Please sign in to comment.