SDKs

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"]
))
FieldTypeRequiredDescription
organizationIdStringyesYour AirStrings organization ID.
projectIdStringyesProject ID.
environmentIdStringyesEnvironment ID (e.g. production, staging).
publicKeys[String]yesOne or more base64-encoded Ed25519 public keys. Multiple keys supported for rotation.
localeAirStringsLocaleno.system (device locale, default) or .fixed("en-US").
apiBaseURLURLnoAPI base URL. Defaults to https://api.airstrings.com.
seedBundleBundlenoBundle 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.
seedSubdirectoryStringnoSeed directory inside the bundle. Defaults to "airstrings/bundles".
isSeedingEnabledBoolnoSet 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

  1. On init, the SDK loads the cached bundle from disk (if any) and fetches the latest from the CDN.
  2. Every bundle is Ed25519-signed. Verification is mandatory: an invalid signature is a hard error and the bundle is rejected.
  3. Verified bundles are cached to Library/Caches/AirStrings/ for offline use and re-verified on every load (defense in depth).
  4. Anti-downgrade protection: a newer revision is never replaced by an older one for the same locale.
  5. The SDK auto-refreshes when the app returns to the foreground, using ETag / If-None-Match to avoid re-downloading unchanged bundles.
  6. 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 pull

Commit 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

SymptomLikely causeFix
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 localePublish a bundle for the locale; confirm organizationId, projectId, and environmentId are correct.
Strings never update after publishingWrong environmentId, or the bundle was published to a different environmentVerify the environment ID matches the one you published from.
Bundle silently rejectedpublicKeys does not contain the key that signed the bundleCopy the environment's public key from Project Settings > SDK Configuration. After rotation, include both old and new keys.

On this page