Amba

iOS Swift SDK

Swift quickstart for the Amba SDK — Swift Package Manager install, Amba.configure, Amba.events.track, Sign in with Apple, and APNs push registration in under 10 minutes.

Amba for iOS is a Swift Package distributed via layers/amba-sdk-ios. Single import; everything else in this guide assumes you've added it to your target.

Supported platforms: iOS 14+, macOS 12+, tvOS 14+, watchOS 7+.

1. Install via Swift Package Manager

In Xcode

File → Add Package Dependencies… → enter:

https://github.com/layers/amba-sdk-ios

Pick the dependency rule Up to Next Major with 1.0.0, then add the Amba product to your app target.

In Package.swift

// Package.swift
let package = Package(
    name: "MyApp",
    platforms: [.iOS(.v14)],
    dependencies: [
        .package(url: "https://github.com/layers/amba-sdk-ios", from: "1.0.0"),
    ],
    targets: [
        .target(
            name: "MyApp",
            dependencies: [
                .product(name: "Amba", package: "amba-sdk-ios"),
            ]
        ),
    ]
)

The package resolves to a prebuilt binary published as a release asset on each tagged version — first build downloads ~12 MB, subsequent builds use Xcode's cache.

2. Configure at app launch

// MyAppApp.swift
import SwiftUI
import Amba
 
@main
struct MyAppApp: App {
    init() {
        do {
            let key = Bundle.main.object(forInfoDictionaryKey: "AmbaClientKey") as? String ?? ""
            try Amba.configure(apiKey: key)
        } catch {
            print("amba: failed to configure — \(error)")
        }
    }
 
    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

For UIKit, do the same in application(_:didFinishLaunchingWithOptions:):

import UIKit
import Amba
 
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        do {
            let key = Bundle.main.object(forInfoDictionaryKey: "AmbaClientKey") as? String ?? ""
            try Amba.configure(apiKey: key)
        } catch {
            print("amba: failed to configure — \(error)")
        }
        return true
    }
}

Don't ship a hard-coded key. Inject AmbaClientKey via your .xcconfig, an Info.plist build-setting, or arkana — and reference it via Bundle.main.object(forInfoDictionaryKey:) or your secrets framework.

Which key goes here

For an iOS app, always use the client key that npx @layers/amba init issued for you (printed as clientKey in the CLI output; visible in the amba console under your project's API keys tab). The client key is designed to ship to end-user devices.

Never put a server key in an iOS build. Server keys are for trusted server contexts only — a server-side cron job, a backend endpoint, an edge function — and grant elevated privileges that would let any reverse-engineered IPA impersonate your tenant. The SDK rejects server keys at configure time with AmbaSwiftError.invalidConfig("server key not allowed in client SDK").

3. Verify the SDK reached amba

Amba.configure(...) is synchronous and only validates the key string locally — a typo or wrong env passes it without complaint. To prove the SDK actually reached the platform, call Amba.diagnostics.ping() and log the server-echoed serverProjectId. Drop this into your @main App's init (or application:didFinishLaunchingWithOptions:) right after Amba.configure:

#if DEBUG
Task {
    do {
        let ping = try await Amba.diagnostics.ping()
        assert(ping.ok, "amba: ping returned ok=false — \(ping.error ?? "unknown")")
        print("amba: connected, project=\(ping.serverProjectId ?? "nil") env=\(ping.environment ?? "nil") key=\(ping.keyFingerprint ?? "nil") latency=\(ping.latencyMs)ms")
    } catch {
        print("amba: verify failed — \(error)")
    }
}
#endif

Build and run. In the Xcode console (View → Debug Area → Activate Console, or ⌘⇧Y):

amba: connected, project=prj_01HZX9... env=production key=a1b2 latency=43ms

If you see that line, your AmbaClientKey resolved to a real project on the server and the wire round-tripped end-to-end. Compare serverProjectId against the project id you think you configured — a mismatch means the wrong .xcconfig is loading. If you don't see the line:

  • No amba: line at allAmba.configure(...) threw before the verify Task ran. Check for the amba: failed to configure — ... log from the catch block in the snippet above. Most common cause: AmbaClientKey is empty (.xcconfig didn't propagate to the build, or the Info.plist key name is misspelled).
  • amba: verify failed — AmbaApiError.unauthorized — the key string is malformed or revoked. Re-run npx @layers/amba init to mint a fresh one and update your .xcconfig.
  • Hangs for >5s with no output — the device can't reach api.amba.dev. Check the Network section of Xcode's debug navigator; a TLS error or DNS failure shows up there.

For CI or test-time gates, escalate the print to an assert/assertionFailure so a misconfiguration fails the build instead of producing a silent log.

4. First sign-in (anonymous + Sign in with Apple)

Amba.events.track is authenticated server-side — the request needs a session token. Sign in (anonymously is fine) before the first track call:

import Amba
 
// Anonymous — minted on first launch, restored from Keychain on subsequent ones.
Task {
    do {
        let session = try await Amba.auth.signInAnonymously()
        print("signed in:", session.user.id)
    } catch {
        print("sign-in failed:", error)
    }
}

5. First event

Once a session exists, track engagement events:

import Amba
 
Task {
    try await Amba.events.track("app_opened", properties: ["source": "deep_link"])
}

Amba.events.track is async throws — wrap in a Task { } from UI code, or await directly from another async context.

Sign in with Apple

import AuthenticationServices
import Amba
 
final class AppleSignInCoordinator: NSObject, ASAuthorizationControllerDelegate {
    func signIn() {
        let request = ASAuthorizationAppleIDProvider().createRequest()
        request.requestedScopes = [.fullName, .email]
        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = self
        controller.performRequests()
    }
 
    func authorizationController(controller: ASAuthorizationController,
                                 didCompleteWithAuthorization authorization: ASAuthorization) {
        guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential,
              let tokenData = credential.identityToken,
              let identityToken = String(data: tokenData, encoding: .utf8) else { return }
 
        Task {
            do {
                let session = try await Amba.auth.signInWithApple(identityToken: identityToken)
                print("signed in:", session.user.id)
            } catch {
                print("Apple sign-in failed:", error)
            }
        }
    }
 
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        print("Apple sign-in error:", error)
    }
}

Add the Sign in with Apple capability in Xcode → Signing & Capabilities before the first build.

6. Register for APNs push

import UIKit
import UserNotifications
import Amba
 
extension AppDelegate {
    func registerForPushNotifications() {
        UNUserNotificationCenter.current().requestAuthorization(
            options: [.alert, .badge, .sound]
        ) { granted, _ in
            guard granted else { return }
            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
            }
        }
    }
 
    func application(_ application: UIApplication,
                     didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let token = deviceToken.map { String(format: "%02x", $0) }.joined()
        Task {
            do {
                _ = try await Amba.push.register(
                    token: token,
                    platform: .apns,
                    bundleId: Bundle.main.bundleIdentifier
                )
            } catch {
                print("amba: push register failed —", error)
            }
        }
    }
 
    func application(_ application: UIApplication,
                     didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("amba: APNs registration failed —", error)
    }
}

Add the Push Notifications capability + Background Modes → Remote notifications in Xcode before building. Upload your APNs auth key (or .p8) to the amba console once per bundle ID.

7. First collection insert

The Swift SDK uses Codable for typed reads + writes:

import Amba
 
struct Post: Codable {
    let id: String
    let title: String
    let body: String
    let createdAt: Date
 
    enum CodingKeys: String, CodingKey {
        case id, title, body
        case createdAt = "created_at"
    }
}
 
Task {
    let inserted: Post = try await Amba.collections.insert(
        "posts",
        row: ["title": "Hello amba", "body": "first post from iOS"],
        as: Post.self
    )
 
    let resp: FindResponse<Post> = try await Amba.collections.find(
        "posts",
        options: FindOptions(
            order: [OrderBy(column: "created_at", direction: .desc)],
            limit: 20
        )
    )
    print("got \(resp.data.count) posts")
}

8. AI proxy

import Amba
 
Task {
    let response = try await Amba.ai.anthropic.messages.create(
        request: AiMessageRequest(
            promptSlug: "support_assistant",
            variables: AnyEncodable(["user_query": "How do I cancel?"]),
            maxTokens: 1024
        )
    )
    print(response.content)
}

Calling from SwiftUI

All SDK methods are async throws. From a View, wrap calls in a Task — either implicit on a button action or explicit in .task { } for view-lifecycle work:

import SwiftUI
import Amba
 
struct ContentView: View {
    @State private var status: String = "tap to track"
 
    var body: some View {
        VStack {
            Text(status)
            Button("Track event") {
                Task {
                    do {
                        try await Amba.events.track("button_tapped")
                        status = "tracked"
                    } catch {
                        status = "error: \(error)"
                    }
                }
            }
        }
        .task {
            // Runs when the view appears; cancelled when it disappears.
            // Use for one-shot setup like a session refresh.
            _ = try? await Amba.auth.signInAnonymously()
        }
    }
}

.task { } is preferred over .onAppear for async work — it ties the Task's lifetime to the view's lifetime, so a fast nav-back cancels the in-flight call cleanly.

App lifecycle (background, foreground, scene phases)

Amba's HTTP client survives backgrounding — there's no special hook to call when the app goes inactive or returns to foreground. A few things to know:

  • Session token refresh happens automatically on the next API call; you don't need to manually refresh on scenePhase == .active.
  • In-flight requests are cancelled when the OS suspends the app. They'll resume on the next call after foreground; if you need at-least-once delivery (e.g. an event triggered just before backgrounding), enqueue it from a BGProcessingTask or your own retry queue rather than firing it directly in applicationWillResignActive.
  • Push registration tokens can rotate when iOS reinstalls the app or restores from backup. Re-call Amba.push.register(...) on every didRegisterForRemoteNotificationsWithDeviceToken — the server treats it as idempotent.

Construct an AmbaClient directly (testing / multi-tenant)

For tests or multi-tenant apps, instantiate AmbaClient directly instead of using the singleton facade:

import Amba
 
let key = Bundle.main.object(forInfoDictionaryKey: "AmbaClientKey") as? String ?? ""
let client = try AmbaClient(apiKey: key)
try await client.events.track("test_event")

Tests can inject a mock via the internal initializer — see the package's Tests/ directory for the protocol shape.

PrivacyManifest

The SDK ships PrivacyInfo.xcprivacy declaring its data-collection categories per Apple's PrivacyManifest spec. App Store review aggregates this with your app's own manifest — you don't need to copy entries from the SDK into yours.

Common pitfalls

  • Events silently vanishAmba.events.track queues, batches, and retries by design; a wrong key, wrong env, or revoked credential does not throw at the call site. Always run the verify step in section 3 on every fresh build (or gate behind #if DEBUG) so a misconfiguration shows up as a loud failure, not silence. This is the failure mode that ends with "I shipped to TestFlight and no events showed up for a week."
  • Wrong env in a build flavour — a clientKey minted for staging will happily configure against the production stack and quietly write to the wrong project. The verify snippet in section 3 prints the server-resolved serverProjectId + environment; compare against the values your build expects on first launch instead of finding the drift in retention dashboards a week later.
  • Amba.configure not called before any other Amba.* access throws AmbaSwiftError.notConfigured. Configure in init of your @main App / application:didFinishLaunchingWithOptions:.
  • Binary not building for simulator — the prebuilt binary includes simulator + device slices for both arm64 and x86_64. If swift build fails with "no compatible binary for the current platform," your Xcode is older than 15.0 — upgrade Xcode.
  • APNs token registered but no delivery — verify the APNs auth key in the amba console matches the bundle ID Xcode signed your app with. The token is valid; the server-side lookup fails 404 if bundle IDs drift.
  • Sign in with Apple identity token expiredcredential.identityToken is short-lived. Always pass it to Amba.auth.signInWithApple immediately; don't cache and reuse.

See also