Contains a single property wrapper, @KeychainStorage, for conviently and securely storing sensitive data in the iOS Keychain.
- Simple, declarative syntax similar to
@AppStorage - Minimalistic implementation—just a single file (should you wish to copy-and-paste!)
- Compatible with Swift 6 and low deployment targets
- Basic Types:
String,Int,Double,Bool - Foundation Types:
URL,Data - Custom Types: Any custom type that that conforms to
Codable
You can integrate KeychainStorageKit into your project using Swift Package Manager:
- In Xcode, select your project in the Project Navigator
- Go to the Package Dependencies tab
- Click the + button to add a package dependency
- In the search bar, enter the repository URL for:
https://github.com/maxhumber/KeychainStorageKit
The syntax of @KeychainStorage is familiar and feels like a close cousin of @AppStorage:
import KeychainStorageKit func setToken(_ newToken: String) { @KeychainStorage("authToken") var token: String? token = newToken } func getToken() -> String? { @KeychainStorage("authToken") var token: String? return token } func removeToken() { @KeychainStorage("authToken") var token: String? token = nil }Here's how you might use the @KeychainStorage wrapper in a SwiftUI app:
import KeychainStorageKit import SwiftUI // MARK: - API Client struct FetchClient { var fetch: @Sendable () async throws -> String static let live = FetchClient( fetch: { @KeychainStorage("token") var token: String? let result = "Result from token: [\(token ?? "missing")]" // Use token return result } ) static let preview = FetchClient( fetch: { try await Task.sleep(for: .seconds(2)) return "Result for preview [no token]" } ) } extension EnvironmentValues { @Entry var fetchClient: FetchClient = .live } // MARK: - View struct ContentView: View { @Environment(\.fetchClient) var client: FetchClient @State var result: String? var body: some View { VStack { Text(result ?? "") Button("Fetch") { Task { await fetch() } } } .task { await tokenRefresh() } } private func fetch() async { result = try? await client.fetch() } private func tokenRefresh() async { while !Task.isCancelled { let newToken = String((0..<5).compactMap { _ in "ABCDEF1234567890".randomElement() }) print("New token: [\(newToken)]") @KeychainStorage("token") var token: String? token = newToken // Set token try? await Task.sleep(for: .seconds(5)) } } } // MARK: - Preview #Preview { ContentView() .environment(\.fetchClient, .live) // .environment(\.fetchClient, .preview) } // MARK: - Entrypoint @main struct KeychainStorageExampleApp: App { var body: some Scene { WindowGroup { ContentView() } } }Due to Keychain access requirements, the tests for this package must run against a "TestHost" app (following this tutorial). To run the tests:
make test