Type-safe data persistence in Swift made simple - with built-in support for UserDefaults, Keychain, in-memory, and file-based storage.
Report Bug
·
Request Feature
LeezyPersistence is a lightweight, type-safe persistence library for Swift.
It provides intuitive property wrappers for saving and retrieving values from:
- 🧠 In-memory storage - fast and temporary
- 🗂️ UserDefaults - for lightweight app preferences
- 🔐 Keychain - for securely storing sensitive values like tokens
- 📁 File-based storage - for custom JSON-encoded persistence
No need to manage different APIs - just use one unified, clean interface.
Inspired by this article on AppStorage.
Add LeezyPersistence to your project using Swift Package Manager:
dependencies: [
.package(url: "https://github.com/matolah/LeezyPersistence.git", .upToNextMajor(from: "1.0.0"))
]- Create your preferences class
final class SharedPreferences: BasePreferences, KeychainPreferences, UserDefaultPreferences {
let keychainManager: KeychainManagerProtocol
let userDefaults: UserDefaults
@UserDefault<Bool, SharedPreferences>("hasSeenTutorial") var hasSeenTutorial: Bool?
@Keychain<String, SharedPreferences>("accessToken") var accessToken: String?
@InMemory<Int, SharedPreferences> var cachedPage: Int?
@File<[String], SharedPreferences>("history.json") var savedHistory: [String]?
init(keychainManager: KeychainManagerProtocol, userDefaults: UserDefaults) {
self.keychainManager = keychainManager
self.userDefaults = userDefaults
super.init()
}
override func handle(error: Error) {
print("Error: \(error)")
}
}- Register it
@main
struct MyApp: App {
init() {
sharedPreferences = SharedPreferences(
keychainManager: MyKeychainManager(),
userDefaults: .standard
)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}- Access preferences with
@Preference
final class ContentViewModel: ObservableObject {
private let userID: String
@Published var isInHomePage = false
@Preference(\SharedPreferences.hasSeenTutorial) var hasSeenTutorial
@Preference(\SharedPreferences.accessToken) var accessToken
private var cancellables = Set<AnyCancellable>()
init(userID: String) {
self.userID = userID
_accessToken.subscribe(storingTo: &cancellables) { [weak self] _ in
guard let self, self.hasSeenTutorial == true else {
return
}
self.isInHomePage = true
}
}
func markTutorialAsSeen() {
// Access user-specific `Preference` using a dynamic key
hasSeenTutorial[userID] = true
}
}- Create as many
Preferencesclasses with context-specific preferences as you'd like:
final class OnboardingPreferences: BasePreferences {
let keychainManager: KeychainManagerProtocol
@Keychain<String, OnboardingPreferences>("accessToken") var accessToken: String?
init(keychainManager: KeychainManagerProtocol) {
self.keychainManager = keychainManager
super.init()
}
override func handle(error: Error) {
print("Error: \(error)")
}
}Distributed under the MIT License. See LICENSE.txt for more information.
Contributions are welcome! If you have suggestions for improvements, bug fixes, or new features, feel free to open an issue or submit a pull request.
Maintained by @_matolah