Skip to content

maxhumber/KeychainStorageKit

Repository files navigation

KeychainStorageKit

Contains a single property wrapper, @KeychainStorage, for conviently and securely storing sensitive data in the iOS Keychain.

Features

  • 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

Supported Types

  • Basic Types: String, Int, Double, Bool
  • Foundation Types: URL, Data
  • Custom Types: Any custom type that that conforms to Codable

Installation

You can integrate KeychainStorageKit into your project using Swift Package Manager:

  1. In Xcode, select your project in the Project Navigator
  2. Go to the Package Dependencies tab
  3. Click the + button to add a package dependency
  4. In the search bar, enter the repository URL for: https://github.com/maxhumber/KeychainStorageKit

Usage

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 }

Usage within a SwiftUI App

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() } } }

Tests

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

About

@KeychainStorage property wrapper

Topics

Resources

License

Stars

Watchers

Forks

Contributors