Merge the UserDefaultsStorage helpers into UserDefaultsProtocol.

This commit is contained in:
Doug
2026-06-26 14:08:38 +01:00
committed by Doug
parent f05fd41c36
commit 66de86b5d2
5 changed files with 96 additions and 159 deletions
@@ -16,3 +16,66 @@ public nonisolated protocol UserDefaultsProtocol: AnyObject {
func reset()
}
// MARK: - Codable handling
nonisolated extension UserDefaultsProtocol {
/// Reads and writes a `Codable` value stored under `key`, used by the `@UserPreference` macro.
///
/// When `Value` conforms to `PlistRepresentable` the Codable encode/decode phase is skipped and
/// the value is stored natively in the plist.
subscript<Value: Codable>(key: String) -> Value? {
get {
if Value.self is PlistRepresentable.Type {
object(forKey: key) as? Value
} else {
data(forKey: key).flatMap { try? JSONDecoder().decode(Value.self, from: $0) }
}
}
set {
// Detects correctly double optionals like this: String?? = .some(nil)
if newValue.isNil {
removeObject(forKey: key)
} else if Value.self is PlistRepresentable.Type {
set(newValue, forKey: key)
} else {
set(try? JSONEncoder().encode(newValue), forKey: key)
}
}
}
}
private nonisolated protocol Nullable {
var isNil: Bool { get }
}
nonisolated extension Optional: Nullable {
var isNil: Bool {
switch self {
case .none:
return true
case .some(let nullable as Nullable):
return nullable.isNil
case .some:
return false
}
}
}
// MARK: - PlistRepresentable
/// A protocol to mark types as being plist compliant.
/// The `UserDefaultsProtocol` storage subscript uses this to avoid encoding/decoding plist compliant values with Codable.
nonisolated protocol PlistRepresentable { }
extension Bool: PlistRepresentable { }
extension String: PlistRepresentable { }
extension Int: PlistRepresentable { }
extension Float: PlistRepresentable { }
extension Double: PlistRepresentable { }
extension Date: PlistRepresentable { }
extension Data: PlistRepresentable { }
extension Array: PlistRepresentable where Element: PlistRepresentable { }
extension Dictionary: PlistRepresentable where Key == String, Value: PlistRepresentable { }
extension Optional: PlistRepresentable where Wrapped: PlistRepresentable { }
@@ -1,109 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Foundation
// MARK: - Storage
/// Typed storage of a `Codable` value in `UserDefaults`, used by the `@UserPreference` macro.
///
/// When used with a `Value` that conforms to `PlistRepresentable` the Codable encode/decode
/// phase is skipped, and values are stored natively in the plist.
nonisolated struct UserDefaultsStorage<Value: Codable> {
private let userDefaults: UserDefaultsProtocol
init(userDefaults: UserDefaultsProtocol) {
self.userDefaults = userDefaults
}
subscript(key: String) -> Value? {
get {
let value: Value?
if Value.self is PlistRepresentable.Type {
value = decodePlistRepresentableValue(for: key)
} else {
value = decodeValue(for: key)
}
return value
}
nonmutating set {
if Value.self is PlistRepresentable.Type {
encodePlistRepresentable(value: newValue, for: key)
} else {
encode(value: newValue, for: key)
}
}
}
private func decodeValue(for key: String) -> Value? {
userDefaults
.data(forKey: key)
.flatMap {
try? JSONDecoder().decode(Value.self, from: $0)
}
}
private func decodePlistRepresentableValue(for key: String) -> Value? {
userDefaults.object(forKey: key) as? Value
}
private func encode(value: Value?, for key: String) {
// Detects correctly double optionals like this: String?? = .some(nil)
if value.isNil {
userDefaults.removeObject(forKey: key)
} else {
let encodedValue = try? JSONEncoder().encode(value)
userDefaults.set(encodedValue, forKey: key)
}
}
private func encodePlistRepresentable(value: Value?, for key: String) {
// Detects correctly double optionals like this: String?? = .some(nil)
if value.isNil {
userDefaults.removeObject(forKey: key)
} else {
userDefaults.set(value, forKey: key)
}
}
}
private nonisolated protocol Nullable {
var isNil: Bool { get }
}
nonisolated extension Optional: Nullable {
var isNil: Bool {
switch self {
case .none:
return true
case .some(let nullable as Nullable):
return nullable.isNil
case .some:
return false
}
}
}
// MARK: - PlistRepresentable
/// A protocol to mark types as being plist compliant.
/// UserDefaultsStorage uses this protocol to avoid to encode/decode with Codable plist compliant values.
nonisolated protocol PlistRepresentable { }
extension Bool: PlistRepresentable { }
extension String: PlistRepresentable { }
extension Int: PlistRepresentable { }
extension Float: PlistRepresentable { }
extension Double: PlistRepresentable { }
extension Date: PlistRepresentable { }
extension Data: PlistRepresentable { }
extension Array: PlistRepresentable where Element: PlistRepresentable { }
extension Dictionary: PlistRepresentable where Key == String, Value: PlistRepresentable { }
extension Optional: PlistRepresentable where Wrapped: PlistRepresentable { }