Skip to content

ricky-stone/SwiftKey

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SwiftKey

Release CI Platforms Swift License

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.

Install

Xcode

  1. Open File > Add Packages...
  2. Add https://github.com/ricky-stone/SwiftKey.git
  3. Choose Up to Next Major from 2.0.0

Package.swift

dependencies: [
    .package(url: "https://github.com/ricky-stone/SwiftKey.git", from: "2.0.0")
]

Quick Start

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.

Supported Values

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.

Optional 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 = nil

Default Store

By 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.

Custom Store

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 = ""
}

iCloud Keychain Sync

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.

Passing a Store in init

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

Errors

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

Beginner API

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

Throws API

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")

Typed Keys

let userKey = SwiftKey.Key<User>("profile.user")

try key.set(userKey, User(name: "Ricky", age: 29))
let user = try key.get(userKey)

Namespaces

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.

Testing

Use InMemorySwiftKeyStore for unit tests.

let store = InMemorySwiftKeyStore()

struct TestSession {
    @KeyChain("token", store: store)
    var token = ""
}

Run the test suite:

swift test

Upgrading to 2.0

Version 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.

Error Glossary

  • 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.

License

MIT License

Copyright (c) 2026 Ricky Stone

About

Beginner-friendly Swift Keychain wrapper for primitives and Codable models across Apple platforms.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages