maniramezan/swiftychain
SwiftyChain is a Swift 6.3 keychain wrapper for Apple platforms. It provides a typed Keychain actor for async-safe access and an @KeychainStorage property wrapper for simple optional values.
Installation
Add SwiftyChain to your Package.swift:
dependencies: [
.package(url: "https://github.com/maniramezan/SwiftyChain", from: "1.0.0"),
],
targets: [
.target(
name: "MyTarget",
dependencies: [
.product(name: "SwiftyChain", package: "SwiftyChain"),
]
),
]Quick Start
import SwiftyChain
let tokenKey = KeychainKey<String>(service: "com.example.app", account: "auth_token")
try await Keychain.shared.upsert("secret", for: tokenKey)
let token = try await Keychain.shared.loadIfPresent(key: tokenKey)@KeychainStorage("auth_token", service: "com.example.app")
var authToken: String?account and service are the required initializer parameters. The full initializer is:
@KeychainStorage(
"auth_token",
service: "com.example.app",
accessGroup: nil,
accessibility: .whenUnlocked,
isSynchronizable: false
)
var authToken: String?Use the optional arguments only when needed:
accessGroupto share credentials with another app target or extensionaccessibilityto control when the item can be readisSynchronizableto opt into iCloud Keychain sync
Platform Support
| Platform | Min Version | |-----------|------------| | macOS | 15.0+ | | iOS | 18.0+ | | iPadOS | 18.0+ | | watchOS | 11.0+ | | tvOS | 18.0+ | | visionOS | 2.0+ |
tvOS note:
whenPasscodeSetThisDeviceOnlyaccessibility is unavailable on tvOS. The#keychainKeymacro emits a warning if you use it on that platform.
Traits
The Swift macros (@KeychainItem, @KeychainScope, #keychainKey) are included by default. Two optional traits gate specialised features:
| Trait | Feature | Enable when | |-------|---------|-------------| | cryptography | kSecClassKey support — store, load, and manage SecKey objects | You need to sign/verify data or work with cryptographic keys | | observation | AsyncSequence-based keychain change stream + @Observable integration | You need to react to keychain changes in real time |
Enable traits in your consuming Package.swift:
.product(name: "SwiftyChain", package: "SwiftyChain", traits: ["observation"])Or when running tests locally:
xcrun swift test --traits "observation,cryptography"@KeychainStorage Limitations
@KeychainStorage is synchronous and safe for @MainActor-isolated types (comparable cost to a UserDefaults read). Do not use it for items with accessibility settings that may trigger user-interaction prompts — keychain calls run on the calling thread and can block it. Use the async Keychain actor for those items instead.
Testing
SwiftyChain ships a separate SwiftyChainTesting product for downstream tests.
import SwiftyChain
import SwiftyChainTesting
let keychain: any KeychainProtocol = InMemoryKeychain()
let tokenKey = KeychainKey<String>(service: "com.example.app", account: "auth_token")
try await keychain.upsert("secret", for: tokenKey)
#expect(try await keychain.load(key: tokenKey) == "secret")import SwiftyChain
import SwiftyChainTesting
let backend = InMemoryKeychainBackend()
let storage = KeychainStorage<String>(
"auth_token",
service: "com.example.app",
backend: backend
)
storage.wrappedValue = "secret"
#expect(storage.wrappedValue == "secret")Development
xcrun swift build -Xswiftc -warnings-as-errors
xcrun swift test -Xswiftc -warnings-as-errors --enable-code-coverage
xcrun swift-format lint --recursive --strict Sources Tests Package.swiftOptional features are guarded by traits. Enable them explicitly when building or testing:
xcrun swift test --traits "observation,cryptography"Set SWIFTYCHAIN_RUN_KEYCHAIN_INTEGRATION=1 to opt into the suite that exercises the real Apple keychain.
Package Metadata
Repository: maniramezan/swiftychain
Default branch: main
README: README.md