Use the system Keychain in AppKit and UIKit as easily as @AppStorage
in SwiftUI — with seamless Codable support and optional value deletion baked in.
- Author: Terence J. Grant
- License: MIT License
- Donate: Your donations are appreciated!
@KeychainStorage
is a property wrapper that reflects a system Keychain value, allowing reactive updates via didSet
when values change.
This is similar to @DefaultsStorage
or @AppStorage
for reading values from UserDefaults
, but useful for reading and writing secure values to the Keychain.
- Simple property wrapper for storing and retrieving secure values. (Passwords, tokens, session IDs, etc.)
- Supports all standard
Codable
types, includingString
,Int
, etc, as well as user-defined,Codable
conforming types. - Provides
didSet
-based reactivity. - Fully tested for reliability.
Add KeychainStorage
to your project with Swift Package Manager in Xcode:
- Go to “File -> Add Package Dependency…”
- Enter the package URL:
https://github.com/tatewake/KeychainStorage
- Use “Dependency Rule -> Exact Version” and set to use release version "1.0.0".
The @KeychainStorage
tag will load from the Keychain on instantiation and save automatically when the values changes. If no value exists in the Keychain, the initializer will provide a default value.
@KeychainStorage("email") var userEmail = "[email protected]"
This syncs automatically on initialization or when modified in code.
Optional values allow you to delete data from the Keychain by setting it to nil
:
@KeychainStorage("sessionID") var sessionID: String?
sessionID = "def456" // Store new value into Keychain
sessionID = nil // Delete item from Keychain
Any type conforming to Codable
works out of the box:
struct User: Codable {
let id: Int
let name: String
}
@KeychainStorage("user") var user: User? = nil
The value is automatically encoded/decoded using JSONEncoder
and JSONDecoder
.
Here’s an example using UserInfo
as a struct
to leverage didSet
reactivity.
struct UserInfo {
struct Login: Equatable {
@DefaultsStorage("username") var username = ""
@DefaultsStorage("password") var password = ""
}
struct Session: Equatable {
@DefaultsStorage("token") var token: String?
@DefaultsStorage("sessionID") var sessionID: String?
}
var login = Login()
var session = Session()
}
Now, in a top-level Model
class
, we use didSet
to handle changes in UserInfo
via a delegate:
class Model {
weak var delegate: MyDelegate?
var userInfo = UserInfo() {
didSet {
if oldValue.login != settings.login {
delegate?.LoginChanged(login: settings.login)
}
if oldValue.session != settings.session {
delegate?.SessionChanged(session: settings.session)
}
}
}
...
}
This is just a simple example, but it shows the straightforward nature of KeychainStorage
.
Much like DefaultsStorage
, there are two things to be aware of:
- External changes won’t update in real time, and
- Properties sharing the same key won’t stay in sync.
This is because @KeychainStorage
only reads from the Keychain on initialization, and only internal code changes trigger the didSet
handler.
Therefore, if another app or process updates your Keychain store, or two @KeychainStorage
properties use the same key, these changes won't trigger reactively.
The first scenario is rare, and for the second, ensure a single variable per setting as the “source of truth.”
- Inspired by SwiftUI’s
@AppStorage
. - Built as a companion to
DefaultsStorage
for handling secure data.