iOS SDK
Fetch, verify, cache, and serve Ed25519-signed string bundles in Swift apps with SwiftUI-first reactivity.
The AirStrings iOS SDK fetches remotely managed localized strings, verifies every bundle's Ed25519 signature before exposing a single string, caches verified bundles for offline use, and updates SwiftUI automatically via Observation.
Requirements: iOS 17+ / macOS 14+, Swift 6.0+, Xcode 16+.
Install
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/symbionix-sl/airstrings-sdk-ios.git", from: "1.1.1")
]Or in Xcode: File > Add Package Dependencies and enter the repository URL.
Setup
Create an AirStrings instance at app launch and keep it alive for the app's lifetime. The instance owns its cache, refresh cycle, and locale. There is no shared singleton.
import AirStrings
let airStrings = AirStrings(configuration: .init(
organizationId: "org_a1b2c3d4e5f6",
projectId: "proj_a1b2c3d4e5f6",
environmentId: "env_a1b2c3d4e5f6",
publicKeys: ["BASE64_ED25519_PUBLIC_KEY"]
))| Field | Type | Required | Description |
|---|---|---|---|
organizationId | String | yes | Your AirStrings organization ID. |
projectId | String | yes | Project ID. |
environmentId | String | yes | Environment ID (e.g. production, staging). |
publicKeys | [String] | yes | One or more base64-encoded Ed25519 public keys. Multiple keys supported for rotation. |
locale | AirStringsLocale | no | .system (device locale, default) or .fixed("en-US"). |
apiBaseURL | URL | no | API base URL. Defaults to https://api.airstrings.com. |
seedBundle | Bundle | no | Bundle probed for the bundled fallback seed. Defaults to .main; pass .module when the seed resource is declared in an SPM library target, or override for app extensions or tests. |
seedSubdirectory | String | no | Seed directory inside the bundle. Defaults to "airstrings/bundles". |
isSeedingEnabled | Bool | no | Set false to disable bundled fallback seeding. Defaults to true. |
Find your organization, project, and environment IDs and the environment's public key in the dashboard under Project Settings > SDK Configuration, or in the setup snippet shown after you publish a bundle.
Usage
SwiftUI
Own the instance with @State and inject it at the root for @Environment access:
import SwiftUI
import AirStrings
@main
struct MyApp: App {
@State private var airStrings = AirStrings(configuration: .init(
organizationId: "org_a1b2c3d4e5f6",
projectId: "proj_a1b2c3d4e5f6",
environmentId: "env_a1b2c3d4e5f6",
publicKeys: ["BASE64_ED25519_PUBLIC_KEY"]
))
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.airStrings, airStrings)
}
}
}
struct ContentView: View {
@Environment(\.airStrings) var strings
var body: some View {
Text(strings["onboarding.welcome_title"])
}
}AirStrings is @Observable. Views that read strings, currentLocale, isReady, or revision re-render automatically when bundles update: no Combine, no manual subscriptions.
ViewModels
Pass the instance into your ViewModel:
@MainActor
@Observable
final class SettingsViewModel {
private let strings: AirStrings
init(strings: AirStrings) {
self.strings = strings
}
var title: String {
strings["settings.title"]
}
}Construct it from a view with the injected instance, e.g. SettingsViewModel(strings: strings). Observation tracks through @Observable ViewModels, so dependent views re-render on updates.
Reading strings
The subscript returns the raw value, or the key name itself as a fallback when no bundle has loaded:
let title = airStrings["onboarding.welcome_title"]Locale
The SDK uses the device locale by default. Fix it at init:
let airStrings = AirStrings(configuration: .init(
organizationId: "org_a1b2c3d4e5f6",
projectId: "proj_a1b2c3d4e5f6",
environmentId: "env_a1b2c3d4e5f6",
publicKeys: ["BASE64_ED25519_PUBLIC_KEY"],
locale: .fixed("fr")
))Or switch at runtime:
await airStrings.setLocale("es")String variants
String variants are A/B experiments: a variant is selected deterministically from a stable assignment id, entirely on-device, with no server round-trip. Set an assignment id once and forward exposures to your analytics:
airStrings.setAssignmentId(currentUser.id)
airStrings.onExposure = { event in
analytics.track("string_exposure", properties: [
"key": event.key,
"experiment_id": event.experimentId,
"variant": event.variant,
"locale": event.locale,
"assignment_id": event.assignmentId
])
}Reads are unchanged: let title = airStrings["onboarding.welcome_title"] returns the assigned variant's value. onExposure is ((ExposureEvent) -> Void)?; the ExposureEvent carries key, experimentId, variant, locale, and assignmentId (all String).
Experiment content is Ed25519-verified via a separate experiments_signature and soft-fails to base values if verification fails: variants are never served unverified. Passing nil to setAssignmentId(_:) clears the assignment and returns to base values. Available since iOS SDK 1.1.0.
See String Variants for how selection, bucketing, and exposure work, and managing experiments from the CLI to create one.
ICU formatting
Every string has a format: "text" (plain) or "icu" (ICU MessageFormat). The subscript always returns the raw value. Use string(_:args:) when you need formatting:
airStrings.string("items.count", args: ["count": 3])text strings are returned as-is (arguments ignored). Pattern syntax: ICU by example.
Caching & offline
- On init, the SDK loads the cached bundle from disk (if any) and fetches the latest from the CDN.
- Every bundle is Ed25519-signed. Verification is mandatory: an invalid signature is a hard error and the bundle is rejected.
- Verified bundles are cached to
Library/Caches/AirStrings/for offline use and re-verified on every load (defense in depth). - Anti-downgrade protection: a newer revision is never replaced by an older one for the same locale.
- The SDK auto-refreshes when the app returns to the foreground, using
ETag/If-None-Matchto avoid re-downloading unchanged bundles. - With no cache and no network, string keys are returned as a fallback. All failure paths are silent: views keep showing the last known strings.
Multiple publicKeys support rotation; see Key rotation.
Bundled fallback
Ship published, signed bundles inside your app so a cold start with no cache and no network serves real strings instead of key names.
Pull the published bundles
airstrings bundles pullCommit and package the seed
Commit the generated airstrings/bundles/ directory and add it to your app target. The directory hierarchy must be preserved by the build system:
- Xcode targets: add the committed
airstrings/folder as a folder reference (blue folder, not a group) - SPM targets: declare
resources: [.copy("airstrings")], never.process, which flattens the hierarchy
Seeding is zero-config when the seed directory is present. Every seed runs the full Ed25519 verification pipeline before use, and the highest verified revision among cache and seed wins. A missing seed directory is a silent no-op; a tampered or mismatched seed is rejected and never applied. Run airstrings bundles pull in CI or as a pre-release step to keep seeds current.
See Offline & fallback for how seeding, caching, and key-name fallback interact.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Strings show as raw keys (e.g. onboarding.welcome_title) | No bundle has loaded yet (no cache + first launch offline), or no bundle is published for this locale | Publish a bundle for the locale; confirm organizationId, projectId, and environmentId are correct. |
| Strings never update after publishing | Wrong environmentId, or the bundle was published to a different environment | Verify the environment ID matches the one you published from. |
| Bundle silently rejected | publicKeys does not contain the key that signed the bundle | Copy the environment's public key from Project Settings > SDK Configuration. After rotation, include both old and new keys. |