mirror of
https://github.com/laosb/TypedAppStorage.git
synced 2025-06-23 17:51:08 +00:00
feat: Add TypedUserDefaults with tests.
This commit is contained in:
parent
b3326032f6
commit
3649c706d9
2 changed files with 238 additions and 0 deletions
91
Sources/TypedAppStorage/TypedUserDefaults.swift
Normal file
91
Sources/TypedAppStorage/TypedUserDefaults.swift
Normal file
|
@ -0,0 +1,91 @@
|
|||
import Foundation
|
||||
|
||||
/// A type-safe wrapper around UserDefaults that supports JSON-based Codable encoding/decoding.
|
||||
///
|
||||
/// `TypedUserDefaults` mimics Apple's UserDefaults API while providing type safety for complex data structures
|
||||
/// that conform to ``TypedAppStorageValue``. Since ``TypedAppStorageValue`` includes the storage key and default value,
|
||||
/// many parameters become optional in the API.
|
||||
public class TypedUserDefaults {
|
||||
/// The underlying UserDefaults store.
|
||||
public let userDefaults: UserDefaults
|
||||
private let encoder = JSONEncoder()
|
||||
private let decoder = JSONDecoder()
|
||||
|
||||
/// The shared typed user defaults object.
|
||||
public static let standard = TypedUserDefaults()
|
||||
|
||||
/// Creates a TypedUserDefaults instance with the specified UserDefaults store.
|
||||
/// - Parameter userDefaults: The underlying UserDefaults store. Defaults to UserDefaults.standard.
|
||||
public init(userDefaults: UserDefaults = .standard) {
|
||||
self.userDefaults = userDefaults
|
||||
}
|
||||
|
||||
// MARK: - Reading Values
|
||||
|
||||
/// Returns the typed object associated with the specified key.
|
||||
/// - Parameters:
|
||||
/// - type: The type of object to retrieve.
|
||||
/// - key: The key to retrieve the value for. If nil, uses the type's appStorageKey.
|
||||
/// - defaultValue: The default value to return if no object exists for the key. If nil, uses the type's default value.
|
||||
/// - Returns: The typed object, or the type's default value if no object exists for the key.
|
||||
public func object<T: TypedAppStorageValue>(
|
||||
of type: T.Type,
|
||||
forKey key: String? = nil,
|
||||
defaultValue: T? = nil
|
||||
) -> T {
|
||||
let storageKey = key ?? T.appStorageKey
|
||||
|
||||
guard let jsonString = userDefaults.string(forKey: storageKey),
|
||||
let data = jsonString.data(using: .utf8),
|
||||
let decodedValue = try? decoder.decode(T.self, from: data)
|
||||
else {
|
||||
return defaultValue ?? T.defaultValue
|
||||
}
|
||||
|
||||
return decodedValue
|
||||
}
|
||||
|
||||
// MARK: - Writing Values
|
||||
|
||||
/// Sets the value of the specified typed object for the given key.
|
||||
/// - Parameters:
|
||||
/// - value: The typed object to store.
|
||||
/// - key: The key to store the value under. If nil, uses the type's appStorageKey.
|
||||
public func set<T: TypedAppStorageValue>(
|
||||
_ value: T,
|
||||
forKey key: String? = nil
|
||||
) {
|
||||
let storageKey = key ?? T.appStorageKey
|
||||
|
||||
guard let data = try? encoder.encode(value),
|
||||
let jsonString = String(data: data, encoding: .utf8)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
userDefaults.set(jsonString, forKey: storageKey)
|
||||
}
|
||||
|
||||
// MARK: - Removing Values
|
||||
|
||||
/// Removes the value associated with the specified key for the given type.
|
||||
/// - Parameters:
|
||||
/// - type: The type of object to remove.
|
||||
/// - key: The key to remove the value for. If nil, uses the type's appStorageKey.
|
||||
public func removeObject<T: TypedAppStorageValue>(
|
||||
of type: T.Type,
|
||||
forKey key: String? = nil
|
||||
) {
|
||||
let storageKey = key ?? T.appStorageKey
|
||||
userDefaults.removeObject(forKey: storageKey)
|
||||
}
|
||||
|
||||
// MARK: - UserDefaults Forwarding
|
||||
|
||||
/// Synchronizes any changes made to the shared user defaults.
|
||||
/// - Returns: true if the synchronization was successful, false otherwise.
|
||||
@discardableResult
|
||||
public func synchronize() -> Bool {
|
||||
return userDefaults.synchronize()
|
||||
}
|
||||
}
|
147
Tests/TypedAppStorageTests/TypedUserDefaultsTests.swift
Normal file
147
Tests/TypedAppStorageTests/TypedUserDefaultsTests.swift
Normal file
|
@ -0,0 +1,147 @@
|
|||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import TypedAppStorage
|
||||
|
||||
// MARK: - Test Models
|
||||
|
||||
struct TestUser: TypedAppStorageValue, Equatable {
|
||||
static var appStorageKey = "testUser"
|
||||
static var defaultValue = TestUser(name: "Anonymous", age: 0)
|
||||
|
||||
let name: String
|
||||
let age: Int
|
||||
}
|
||||
|
||||
struct TestSettings: TypedAppStorageValue, Equatable {
|
||||
static var appStorageKey = "testSettings"
|
||||
static var defaultValue = TestSettings(theme: .light, notifications: true)
|
||||
|
||||
enum Theme: String, Codable {
|
||||
case light, dark
|
||||
}
|
||||
|
||||
let theme: Theme
|
||||
let notifications: Bool
|
||||
}
|
||||
|
||||
// MARK: - Tests
|
||||
|
||||
@Suite("TypedUserDefaults Tests")
|
||||
struct TypedUserDefaultsTests {
|
||||
|
||||
// Helper to create a clean test environment
|
||||
private func createTestDefaults() -> TypedUserDefaults {
|
||||
let userDefaults = UserDefaults()
|
||||
userDefaults.removePersistentDomain(forName: Bundle.main.bundleIdentifier ?? "test")
|
||||
return TypedUserDefaults(userDefaults: userDefaults)
|
||||
}
|
||||
|
||||
@Test("Should return default value when no stored value exists")
|
||||
func testDefaultValueRetrieval() {
|
||||
let typedDefaults = createTestDefaults()
|
||||
|
||||
let user = typedDefaults.object(of: TestUser.self)
|
||||
|
||||
#expect(user == TestUser.defaultValue)
|
||||
#expect(user.name == "Anonymous")
|
||||
#expect(user.age == 0)
|
||||
}
|
||||
|
||||
@Test("Should store and retrieve typed objects correctly")
|
||||
func testStoreAndRetrieve() {
|
||||
let typedDefaults = createTestDefaults()
|
||||
let testUser = TestUser(name: "John Doe", age: 30)
|
||||
|
||||
// Store the value
|
||||
typedDefaults.set(testUser)
|
||||
|
||||
// Retrieve the value
|
||||
let retrievedUser = typedDefaults.object(of: TestUser.self)
|
||||
|
||||
#expect(retrievedUser == testUser)
|
||||
#expect(retrievedUser.name == "John Doe")
|
||||
#expect(retrievedUser.age == 30)
|
||||
}
|
||||
|
||||
@Test("Should handle custom keys and default values")
|
||||
func testCustomKeysAndDefaults() {
|
||||
let typedDefaults = createTestDefaults()
|
||||
let customUser = TestUser(name: "Jane Smith", age: 25)
|
||||
let customDefault = TestUser(name: "Custom Default", age: 99)
|
||||
let customKey = "customUserKey"
|
||||
|
||||
// Store with custom key
|
||||
typedDefaults.set(customUser, forKey: customKey)
|
||||
|
||||
// Retrieve with custom key and custom default
|
||||
let retrievedUser = typedDefaults.object(
|
||||
of: TestUser.self, forKey: customKey, defaultValue: customDefault)
|
||||
|
||||
#expect(retrievedUser == customUser)
|
||||
|
||||
// Test retrieval from non-existent key with custom default
|
||||
let nonExistentUser = typedDefaults.object(
|
||||
of: TestUser.self, forKey: "nonExistentKey", defaultValue: customDefault)
|
||||
|
||||
#expect(nonExistentUser == customDefault)
|
||||
#expect(nonExistentUser.name == "Custom Default")
|
||||
#expect(nonExistentUser.age == 99)
|
||||
}
|
||||
|
||||
@Test("Should remove stored objects correctly")
|
||||
func testRemoveObject() {
|
||||
let typedDefaults = createTestDefaults()
|
||||
let testUser = TestUser(name: "To Be Removed", age: 40)
|
||||
|
||||
// Store the value
|
||||
typedDefaults.set(testUser)
|
||||
|
||||
// Verify it's stored
|
||||
let storedUser = typedDefaults.object(of: TestUser.self)
|
||||
#expect(storedUser == testUser)
|
||||
|
||||
// Remove the value
|
||||
typedDefaults.removeObject(of: TestUser.self)
|
||||
|
||||
// Verify it returns default value after removal
|
||||
let removedUser = typedDefaults.object(of: TestUser.self)
|
||||
#expect(removedUser == TestUser.defaultValue)
|
||||
#expect(removedUser.name == "Anonymous")
|
||||
#expect(removedUser.age == 0)
|
||||
}
|
||||
|
||||
@Test("Should handle complex data types and synchronization")
|
||||
func testComplexTypesAndSynchronization() {
|
||||
let typedDefaults = createTestDefaults()
|
||||
let testSettings = TestSettings(theme: .dark, notifications: false)
|
||||
|
||||
// Store complex type
|
||||
typedDefaults.set(testSettings)
|
||||
|
||||
// Test synchronization
|
||||
let syncResult = typedDefaults.synchronize()
|
||||
#expect(syncResult == true)
|
||||
|
||||
// Retrieve and verify complex type
|
||||
let retrievedSettings = typedDefaults.object(of: TestSettings.self)
|
||||
#expect(retrievedSettings == testSettings)
|
||||
#expect(retrievedSettings.theme == .dark)
|
||||
#expect(retrievedSettings.notifications == false)
|
||||
|
||||
// Test with custom key for complex type
|
||||
let customKey = "customSettings"
|
||||
typedDefaults.set(testSettings, forKey: customKey)
|
||||
|
||||
let customRetrievedSettings = typedDefaults.object(of: TestSettings.self, forKey: customKey)
|
||||
#expect(customRetrievedSettings == testSettings)
|
||||
|
||||
// Remove with custom key
|
||||
typedDefaults.removeObject(of: TestSettings.self, forKey: customKey)
|
||||
|
||||
let removedCustomSettings = typedDefaults.object(of: TestSettings.self, forKey: customKey)
|
||||
#expect(removedCustomSettings == TestSettings.defaultValue)
|
||||
#expect(removedCustomSettings.theme == .light)
|
||||
#expect(removedCustomSettings.notifications == true)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue