Simple Keychain storage for Apple platforms.
SwiftKey is built for the common case first:
@KeyChain("token")
private var token = ""If the value exists in Keychain, SwiftKey loads it. If it is missing or cannot be decoded, SwiftKey gives you the default value you wrote in code.
- Open
File > Add Packages... - Add
https://github.com/ricky-stone/SwiftKey.git - Choose
Up to Next Majorfrom2.0.0
dependencies: [
.package(url: "https://github.com/ricky-stone/SwiftKey.git", from: "2.0.0")
]Put @KeyChain inside a type, such as a SwiftUI view, view model, settings object, or app service.
import SwiftKey
struct Session {
@KeyChain("token")
var token = ""
@KeyChain("launchCount")
var launchCount = 0
@KeyChain("isPro")
var isPro = false
}
var session = Session()
session.token = "abc123"
session.launchCount += 1
print(session.token)That is it. Reads and writes go to Keychain automatically.
SwiftKey works with common values:
@KeyChain("username") var username = ""
@KeyChain("launchCount") var launchCount = 0
@KeyChain("taxRate") var taxRate = 0.2
@KeyChain("isPro") var isPro = false
@KeyChain("avatar") var avatar = Data()It also works with your own Codable models:
struct User: Codable {
let name: String
let age: Int
}
@KeyChain("user")
var user = User(name: "Guest", age: 0)Data is stored as raw bytes, so it works well for tokens, certificates, small encrypted blobs, and other binary values.
Use an optional when a missing value should be nil.
@KeyChain("token")
var token: String?
@KeyChain("avatar")
var avatar: Data?Setting an optional value to nil removes it from Keychain:
token = "abc123"
token = nilBy default, @KeyChain uses local Keychain storage with sync turned off.
@KeyChain("token")
var token = ""The default is:
SwiftKey(
service: SwiftKey.defaultService,
synchronizable: false,
accessibility: .afterFirstUnlock
)This is the easiest and most reliable setup for most apps.
Pass a store when you want a custom service name, access group, sync setting, or test store.
let store = SwiftKey(
service: "com.example.myapp",
synchronizable: false
)
struct Secrets {
@KeyChain("token", store: store)
var token = ""
}Sync is opt-in.
let syncedStore = SwiftKey(
service: "com.example.myapp",
synchronizable: true
)
struct SyncedSecrets {
@KeyChain("token", store: syncedStore)
var token = ""
}If sync is unavailable because of entitlements or device state, SwiftKey falls back to local storage.
If your store is created at runtime, assign the wrapper in your initializer:
struct Session {
@KeyChain("token", default: "")
var token: String
init(store: SwiftKeyStore) {
_token = KeyChain("token", default: "", store: store)
}
}The property wrapper does not throw. It returns your default value and keeps the last error on the projected value.
@KeyChain("token")
var token = ""
token = "abc123"
if let error = $token.lastError {
print(error.localizedDescription)
}Use result helpers when you want explicit success or failure:
let result = $token.setResult("abc123")
switch result {
case .success:
print("Saved")
case .failure(let error):
print(error.localizedDescription)
}If you do not want property wrappers, SwiftKey.Beginner is still available.
let key = SwiftKey.Beginner()
key.setString("token", "abc123")
let token = key.getString("token", default: "")
if let error = key.lastErrorMessage {
print(error)
}Use SwiftKey directly when you want try/catch.
let key = SwiftKey()
try key.set("token", "abc123")
let token: String? = try key.get("token")
try key.setData(Data([0x10, 0x20]), forKey: "blob")
let blob = try key.getData(forKey: "blob")let userKey = SwiftKey.Key<User>("profile.user")
try key.set(userKey, User(name: "Ricky", age: 29))
let user = try key.get(userKey)let key = SwiftKey()
let auth = key.namespace("auth")
try auth.set("token", "abc123")
let token = try auth.get("token", as: String.self)The stored key is auth.token.
Use InMemorySwiftKeyStore for unit tests.
let store = InMemorySwiftKeyStore()
struct TestSession {
@KeyChain("token", store: store)
var token = ""
}Run the test suite:
swift testVersion 2.0 adds the @KeyChain property wrapper and changes default storage to local-only.
If you want iCloud Keychain sync, pass synchronizable: true:
let key = SwiftKey(synchronizable: true)Existing values remain readable as long as you keep the same service name, key name, and sync setting.
invalidKey: the key is empty or only whitespace.duplicateKey: the target key already exists when overwrite is not allowed.keyNotFound: the requested key does not exist.encodingFailed: the value could not be encoded.decodingFailed: the saved value does not match the requested type.unhandledStatus(code): the underlying Security framework returned an unexpected status.
MIT License
Copyright (c) 2026 Ricky Stone