Skip to content

matolah/LeezyPersistence

Repository files navigation

LeezyPersistence

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

What is LeezyPersistence?

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.

Installation

Add LeezyPersistence to your project using Swift Package Manager:

dependencies: [
    .package(url: "https://github.com/matolah/LeezyPersistence.git", .upToNextMajor(from: "1.0.0"))
]

Usage

  1. 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)")
    }
}
  1. Register it
@main
struct MyApp: App {
    init() {
        sharedPreferences = SharedPreferences(
            keychainManager: MyKeychainManager(),
            userDefaults: .standard
        )
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
  1. 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
    }
}
  1. Create as many Preferences classes 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)")
    }
}

License

Distributed under the MIT License. See LICENSE.txt for more information.

🤝 Contributing

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.

💬 Contact

Maintained by @_matolah

About

Type-safe data persistence in Swift made simple.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published