String Catalogs compile your translations into the app, so changing a word means a new build and App Store review. To change SwiftUI text after release, load strings at runtime: the AirStrings iOS SDK fetches Ed25519-signed bundles from a CDN, verifies them with CryptoKit on the device, caches them for offline use, and re-renders your views through @Observable when new copy arrives.
This guide covers setup, SwiftUI and view model usage, ICU plurals, the offline cache, signature verification and the bundled fallback, and when String Catalogs alone are the better choice.
String Catalogs vs remote strings
String Catalogs (.xcstrings, Xcode 15 and later) are Apple’s built-in way to localize: Xcode extracts strings from your code, you translate them in the editor, and they ship inside the app bundle. They are the right default. What they cannot do is change after the binary is built.
| String Catalogs only | AirStrings iOS SDK | |
|---|---|---|
| Where strings live | Compiled into the app | Signed bundles on a CDN, cached on device, optional bundled copy |
| Change a word after release | New build and App Store review | Publish; devices update on next refresh |
| Plurals | Plural and device variations in the catalog | ICU MessageFormat plurals and selects |
| Works offline | Always | Cached bundle, or signed bundles shipped in the app |
| Integrity | Covered by the app’s code signature | Ed25519 signature on every bundle, verified before use |
| Extra dependency | None | Swift package, iOS 17+ / macOS 14+ |
Install
Add the package in Xcode (File > Add Package Dependencies) or in Package.swift:
.package(url: "https://github.com/symbionix-sl/airstrings-sdk-ios.git", from: "1.2.0")The SDK needs iOS 17 or macOS 14, Swift 6 and Xcode 16.
Use it in SwiftUI
Own one instance at the app root and inject it into the environment. The ids and public key come from airstrings sdk-config --env production.
import AirStrings
import SwiftUI
@main
struct MyApp: App {
@State private var airStrings = AirStrings(configuration: .init(
organizationId: "org_xxx",
projectId: "proj_xxx",
environmentId: "env_xxx",
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 {
VStack {
Text(strings["onboarding.welcome_title"])
Text(strings.string("cart.items", args: ["count": 3]))
}
}
}AirStrings is @Observable, so any view that reads a string re-renders when a newer bundle is applied. No Combine and no manual subscriptions. The subscript returns the raw value; string(_:args:) formats ICU patterns such as {count, plural, one {# item} other {# items}}.
In a view model
The same tracking works through an @Observable view model that reads from the injected instance:
@MainActor
@Observable
final class SettingsViewModel {
private let strings: AirStrings
init(strings: AirStrings) {
self.strings = strings
}
var title: String {
strings["settings.title"]
}
}Locale
The SDK follows the device locale by default. Pin one with locale: .fixed("fr") in the configuration, or switch at runtime with await airStrings.setLocale("es").
Offline cache and signature verification
- Every bundle is signed with Ed25519 on the platform. There is no unsigned delivery path.
- The SDK verifies before it renders, using Apple CryptoKit and the public keys you passed in. A bad signature, an unknown key id or an unsupported format is a hard error, and the last verified strings stay on screen.
- The signature covers project, locale, revision, format version and creation time, so a bundle cannot be moved to another project or replaced with an older revision.
- Verified bundles are cached in
Library/Caches/AirStrings/and re-verified every time they are loaded. - Refresh is automatic at launch and when the app returns to the foreground, with
ETagrequests so unchanged bundles are not downloaded again. Callawait airStrings.refresh()to trigger it yourself.
To rotate signing keys, list both the old and new public key in publicKeys; the SDK picks the right one from the key_id in each bundle.
Bundled fallback
On a fresh install with no network there is no cache. Ship your published bundles in the app so the first launch shows real copy instead of key names:
airstrings bundles pullCommit the generated airstrings/bundles/ folder and add the airstrings folder to your app target as a folder reference (blue folder), or as resources: [.copy("airstrings")] in a Swift package. The folder structure must be preserved. If the resources live in a library target, pass seedBundle: .module in the configuration.
Seeding is automatic when the folder is present. Each seed file runs the same Ed25519 verification as a download plus project and locale checks, and the highest verified revision across cache and seed wins. Run airstrings bundles pull in CI or before each release so the shipped copy stays current.
Publishing a change
brew install symbionix-sl/airstrings/airstrings
airstrings init <staging-write-key>
airstrings strings set onboarding.welcome_title en="Welcome back" fr="Bon retour" --format text --push
airstrings publish
airstrings promote previewpublish goes to staging; point a TestFlight or debug build at the staging environment to check the copy. Production is protected, so promote preview prints the diff and the dashboard link where a person promotes it. Your release build uses the production ids and keys from airstrings sdk-config --env production.
When to stay with String Catalogs only
- Copy rarely changes after release. A catalog has no runtime dependency and nothing to fetch.
- You support iOS 16 or earlier. The SDK requires iOS 17.
- Every wording change needs per-release compliance review. OTA updates skip the build where that review happens. If you adopt them anyway, keep production protected and promote reviewed changes from staging.
- You want Xcode’s extraction and translation editor. AirStrings does not read
.xcstrings; keys are managed in the dashboard, CLI or CSV import.
The free plan includes signed over-the-air delivery for 1 project, 1,000 keys, 3 locales and 2 environments. For Android, React Native and web, see How to update app strings without an App Store release and React Native OTA translations. The SDK source is on GitHub.
FAQ
Can String Catalogs be updated without an App Store release?
No. A String Catalog (.xcstrings) is compiled into the app bundle, so changing a translation needs a new build and review. Remote strings are loaded at runtime, so they can change after release.
Which iOS versions does the AirStrings SDK support?
iOS 17 and later, and macOS 14 and later, with Swift 6 and Xcode 16. It relies on the Observation framework so SwiftUI views update automatically.
How does the SDK verify strings?
Each bundle carries an Ed25519 signature over its content, project, locale, revision and creation time. The SDK verifies it with Apple CryptoKit against public keys you configure, and rejects bundles that fail. Cached bundles are re-verified on every load.
What does the app show offline?
The last verified bundle from the on-disk cache. On a first launch with no network it uses signed bundles you ship in the app with airstrings bundles pull. Only if neither exists does it show key names.
Can I keep String Catalogs and add remote strings?
Yes. Keep compiled strings for text that never changes and read the copy you want to edit after release from AirStrings.