Contents

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:

  • accessGroup to share credentials with another app target or extension
  • accessibility to control when the item can be read
  • isSynchronizable to 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: whenPasscodeSetThisDeviceOnly accessibility is unavailable on tvOS. The #keychainKey macro 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.swift

Optional 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