Skip to content

Commit

Permalink
Storing Codable data on UserDefaults (#287)
Browse files Browse the repository at this point in the history
  • Loading branch information
EgzonArifi authored Nov 12, 2024
1 parent a643c86 commit 1329997
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 31 deletions.
10 changes: 10 additions & 0 deletions Resources/Utilities/PropertyWrapper/UserDefault/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ struct Flags {
}
```

#### Resetting Values

If you need to reset a value to its default state, the `UserDefault` property wrapper provides a `resetValue` method. This method sets the value for the specified key back to its initial default value, allowing you to easily revert any changes made to the property.

Here's how you can use it:

```swift
Flags.$screenFlag.resetValue()
```

## Tips

Since you can share data between apps, we recommend to created a shared instance of `UserDefaults` on your app:
Expand Down
70 changes: 45 additions & 25 deletions Sources/Utilities/PropertyWrapper/UserDefault.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,63 @@

import Foundation

private protocol OptionalProtocol {
func isNil() -> Bool
}

extension Optional : OptionalProtocol {
func isNil() -> Bool {
return self == nil
}
}

@propertyWrapper public struct UserDefault<Value> {
private let key: String
private let defaultValue: Value
@propertyWrapper
public struct UserDefault<Value: Codable> {
private let storage: UserDefaults

private let keyObject: UserDefaultKey<Value>
private let encoder: JSONEncoder
private let decoder: JSONDecoder

public var wrappedValue: Value {
get {
guard let value = storage.object(forKey: key) else {
return defaultValue
}

return value as? Value ?? defaultValue
guard let data = storage.data(forKey: keyObject.key) else { return keyObject.defaultValue }
let value = try? decoder.decode(Value.self, from: data)
return value ?? keyObject.defaultValue
}
set {
if let value = newValue as? OptionalProtocol, value.isNil() {
storage.removeObject(forKey: key)
} else {
storage.set(newValue, forKey: key)
if let encoded = try? encoder.encode(newValue) {
storage.set(encoded, forKey: keyObject.key)
}
}
}

public init(defaultValue: Value,
key: String,
storage: UserDefaults = .standard) {
self.defaultValue = defaultValue
storage: UserDefaults = .standard,
encoder: JSONEncoder = .init(),
decoder: JSONDecoder = .init()) {
self.storage = storage
self.encoder = encoder
self.decoder = decoder
self.keyObject = UserDefaultKey(key: key,
defaultValue: defaultValue,
storage: storage,
encoder: encoder)
}

public var projectedValue: UserDefaultKey<Value> {
keyObject
}
}

public class UserDefaultKey<Value: Codable> {
let key: String
let defaultValue: Value
let storage: UserDefaults
let encoder: JSONEncoder

init(key: String,
defaultValue: Value,
storage: UserDefaults,
encoder: JSONEncoder) {
self.key = key
self.defaultValue = defaultValue
self.storage = storage
self.encoder = encoder
}

public func resetValue() {
guard let encoded = try? encoder.encode(defaultValue) else { return }
storage.set(encoded, forKey: key)
}
}
63 changes: 57 additions & 6 deletions Tests/Tests/Core/Utilities/UserDefaults/UserDefaultTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class UserDefaultTests: XCTestCase {
try super.tearDownWithError()
userDefaults.removeObject(forKey: Defaults.testBoolKey)
userDefaults.removeObject(forKey: Defaults.testStringKey)
userDefaults.removeObject(forKey: Defaults.testDataKey)
userDefaults.removeObject(forKey: Defaults.testDataModelKey)
}

func testSaveStringValue() {
Expand All @@ -25,7 +27,7 @@ class UserDefaultTests: XCTestCase {
// When
Defaults.screenName = givenValue
// Then
XCTAssertEqual(userDefaults.string(forKey: Defaults.testStringKey), Defaults.screenName)
XCTAssertEqual(givenValue, Defaults.screenName)
}

func testSaveBoolValue() {
Expand All @@ -34,31 +36,72 @@ class UserDefaultTests: XCTestCase {
// When
Defaults.isAuthenticated = givenValue
// Then
XCTAssertEqual(userDefaults.bool(forKey: Defaults.testBoolKey), Defaults.isAuthenticated)
XCTAssertEqual(givenValue, Defaults.isAuthenticated)
}

func testResetValue() {
// Given
let givenValue = Defaults.testStringKey

// Set an initial value
Defaults.screenName = givenValue

// When
Defaults.$screenName.resetValue()

// Then
XCTAssertEqual(Defaults.screenName, "default")
}

func testSaveDataValue() {
// Given
let givenValue: Data = .init()

// When
Defaults.profileData = givenValue

// Then
XCTAssertNotNil(Defaults.profileData)
}

func testSaveDataNullValue() {
// Given
let givenValue: Data? = nil

// When
Defaults.profileData = givenValue

// Then
XCTAssertNil(Defaults.profileData)
}

func testSaveCodable() {
// Given
let givenValue = TestDataModel(id: UUID().uuidString, number: 123)
// When
Defaults.dataModel = givenValue
// Then
XCTAssertEqual(givenValue, Defaults.dataModel)
}

func testResetValueForCodable() {
// Given
let givenValue = TestDataModel(id: UUID().uuidString, number: 123)

// Set an initial value
Defaults.dataModel = givenValue

// When
Defaults.$dataModel.resetValue()

// Then
XCTAssertEqual(Defaults.dataModel.id, "1")
XCTAssertEqual(Defaults.dataModel.number, 1)
}
}

extension UserDefaultTests {
struct Defaults {
static var testBoolKey = "test_bool_key"
static var testStringKey = "test_string_key"
static var testDataKey = "test_data_key"
static var testDataModelKey = "test_data_model_key"

@UserDefault(defaultValue: false, key: testBoolKey)
static var isAuthenticated: Bool
Expand All @@ -68,5 +111,13 @@ extension UserDefaultTests {

@UserDefault(defaultValue: nil, key: testDataKey)
static var profileData: Data?

@UserDefault(defaultValue: TestDataModel(id: "1", number: 1), key: testDataModelKey)
static var dataModel: TestDataModel
}

struct TestDataModel: Codable, Equatable {
let id: String
let number: Int
}
}

0 comments on commit 1329997

Please sign in to comment.