# iOS SDK quickstart

> Source: https://cross-deck.com/docs/quickstart-ios/ · Crossdeck developer docs (generated from the published page).

Quickstart
10 min
·
Swift & SwiftUI · iOS 13+ · iPadOS · macOS · tvOS · watchOS

From a fresh Crossdeck account to your first iOS event landing in the dashboard in under ten minutes. One Swift Package, two values to paste, one `Crossdeck.start(...)` call.

You can also do this from the onboarding wizard.

If you just signed up, the dashboard's **Onboarding** flow generates the same snippet you'll build here. This page exists for developers who closed the wizard, came back later, or want to read the steps before they paste anything into Xcode.

## Before you start

You'll need:

- A Crossdeck account with a project created (if you don't have one, [start free](https://cross-deck.com/start-free/) — no credit card).

- **Xcode 15+**. Check via **Xcode menu → About Xcode**; the version sits at the top of the dialog. Older versions of Xcode may resolve the Swift Package, but our test matrix is 15 and up.

- An iOS app project with a **deployment target of iOS 13.0 or higher**. Check via **your project in the Project navigator → app target → General → Deployment Info → iOS Deployment Target**. (StoreKit purchase-rail features need 15+ at runtime; analytics, identity, errors all work on 13.)

- About 10 minutes.

You don't need an App Store Connect account or a paid Apple Developer membership to send your first event — the simulator works fine. App Store rail connection comes later, in [Connect App Store](https://cross-deck.com/docs/connect-apple/).

## Step 1 · Find your appId and publishable key

Crossdeck identifies your iOS app with two values:

Value What it looks like What it does

appId
app_ios_92b2d6a5728a4d
Identifies which app inside your project the events belong to. Stable, never rotates.

publicKey
cd_pub_test_… or cd_pub_live_…
Authenticates the SDK to the ingest endpoint. Safe to ship inside an App Store binary.

Test keys for development, live keys for App Store builds.

A `cd_pub_test_…` key routes into the sandbox environment — events here never affect production analytics or your customer-facing dashboards. Pair test keys with `environment: .sandbox` in DEBUG builds and live keys with `environment: .production` in RELEASE — the SDK validates the pairing at `start`.

### Where to find them

- Open the [Crossdeck dashboard](https://app.cross-deck.com/) and select your project from the workspace switcher (top-left).

- From the left sidebar, click **Developers**, then **API**. The direct path is [/developers/api](https://app.cross-deck.com/developers/api).

- The page lists your apps. If you haven't added an iOS app yet, click **+ Add app** and pick *iOS*. Each app card shows the `appId` at the top and two publishable keys below — one **test**, one **live**. Click the copy icon next to either to copy it to your clipboard.

## Step 2 · Find your Apple bundle identifier

When you add an iOS app in the Crossdeck dashboard, you'll be asked for its **bundle identifier** — Apple's globally-unique reverse-DNS name for your app (`com.acme.notes`, `com.spotify.client`). Crossdeck uses it to attribute App Store purchases and Apple App Store Server Notifications to the right app inside your project.

### How to find it in Xcode

- Open your project in Xcode.

- In the **Project navigator** (left sidebar), click your project at the top — the entry with the blue Xcode icon (the `.xcodeproj`).

- The main editor now shows project settings. On the left side of that editor, under **TARGETS**, click your *app target* (the one with the iOS app icon, not the test targets).

- Along the top tab bar of the editor, click **General** (the leftmost tab).

- Look at the **Identity** section near the top. The **Bundle Identifier** field is right there — usually shaped like `com.yourcompany.yourapp`.

If you have multiple targets (app, app extension, widget), use the main app target.

Extensions and widgets have their own bundle identifiers, usually shaped like `com.yourcompany.yourapp.widget`. The Crossdeck app entry should match the *main app* bundle ID — the one users see in the App Store. Telemetry from extensions can route to the same Crossdeck app or to a separate one; that's a settings decision later.

### If you can't find Xcode (or want the answer from the filesystem)

The bundle ID lives in your target's `Info.plist` as `CFBundleIdentifier`. From the project directory:

```
plutil -p YourApp/Info.plist | grep CFBundleIdentifier
```

If your project uses Xcode's auto-generated Info.plist (no separate file), the bundle ID lives in `YourProject.xcodeproj/project.pbxproj` under the key `PRODUCT_BUNDLE_IDENTIFIER`.

## Step 3 · Add the Swift Package

### Via Xcode UI (recommended)

- In Xcode, click **File → Add Package Dependencies…**

- Paste the URL into the search field (top-right of the dialog): 

```
https://github.com/Crossdeckhq/crossdeck-swift.git
```

- In the **Dependency Rule** dropdown on the right, select **"Up to Next Major Version"** and enter `1.0.0` — SwiftPM then resolves the latest 1.x automatically (you don't bump this per release; it only changes if v2.0.0 ever lands). **Do not leave it on "Branch: main"** — branch tracking auto-pulls every commit, including breaking changes when v2.0.0 lands.

- Click **Add Package**. Xcode resolves the package and offers to add the `Crossdeck` library product to your app target — accept.

Already had Crossdeck installed? Pull the latest version before you build.

Xcode pins the exact version it first downloaded and **won't** move forward on its own — even with the "Up to Next Major Version" rule set. So if you previously built against an older release and a newly-documented method reports *"has no member …"*, you're compiling against the old pinned tag, not hitting an SDK bug.

**One-button fix:** **File → Packages → Update to Latest Package Versions**. Xcode re-resolves to the newest tag and rewrites `Package.resolved` (the title bar shows *"Checking out crossdeck-swift @ …"*). If it sticks on the old tag, run **File → Packages → Reset Package Caches** first, then Update again.

### Via Package.swift (if you're managing dependencies in code)

```
dependencies: [
    .package(url: "https://github.com/Crossdeckhq/crossdeck-swift.git", from: "1.0.0"),
],
targets: [
    .target(
        name: "MyApp",
        dependencies: [
            .product(name: "Crossdeck", package: "crossdeck-swift"),
        ]
    ),
]
```

`from: "1.0.0"` is the SwiftPM shorthand for "Up to Next Major Version" — it resolves the latest 1.x automatically (never a breaking 2.0), so you never hand-bump it. The product you `import` is `Crossdeck` (no scope); the package on disk is `crossdeck-swift`.

## Step 4 · Initialise the SDK

Call `Crossdeck.start(...)` exactly once at app launch. The canonical place is your `@main App`'s `init()` (SwiftUI) or `application(_:didFinishLaunchingWithOptions:)` (UIKit). Wrap it in `do`/`catch` and store the result as `Optional` — a typo'd key should log + degrade telemetry, never crash a customer's launch.

### SwiftUI ( @main App )

```
import SwiftUI
import Crossdeck

@main
struct MyApp: App {
    static let cd: Crossdeck? = {
        do {
            return try Crossdeck.start(options: CrossdeckOptions(
                appId: "app_ios_92b2d6a5728a4d",
                publicKey: "cd_pub_test_…",
                environment: .sandbox
            ))
        } catch {
            // invalid key, env mismatch — log + degrade, never crash
            print("Crossdeck start failed: \(error)")
            return nil
        }
    }()

    var body: some Scene {
        WindowGroup { ContentView() }
    }
}
```

Reach the client from anywhere as `MyApp.cd?.…`:

```
MyApp.cd?.track("paywall_seen", properties: ["variant": "annual"])

if MyApp.cd?.isEntitled("pro") == true {
    showProUI()
}
```

### UIKit (AppDelegate)

```
import UIKit
import Crossdeck

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    static var cd: Crossdeck?

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: …) -> Bool {
        do {
            AppDelegate.cd = try Crossdeck.start(options: CrossdeckOptions(
                appId: "app_ios_92b2d6a5728a4d",
                publicKey: "cd_pub_test_…",
                environment: .sandbox
            ))
        } catch {
            print("Crossdeck start failed: \(error)")
        }
        return true
    }
}
```

The environment case must match the publishable-key prefix.

`cd_pub_test_…` pairs with `.sandbox`; `cd_pub_live_…` pairs with `.production`. Mismatched values throw at `start` rather than silently routing events into the wrong warehouse. Use `#if DEBUG` to swap both together.

## Step 5 · SwiftUI bolt-ons (two modifiers)

The boot above gives you sessions, errors, identity, and a durable batched event queue for free. **Two dashboard surfaces — Pages and Top Actions — need one modifier per screen and one per important button** on iOS 16+ SwiftUI.

This isn't a Crossdeck regression; SwiftUI's accessibility-merge model genuinely hides screen names and button labels from any UIView-level runtime introspection. Mixpanel, Amplitude, and PostHog all ship the same per-surface modifier pattern.

### .crossdeckScreen("Name") on every destination view

Populates the **Pages** dashboard ("Create Image · 63 views · 14 visitors"). Without it, the SDK's UIKit auto-track has nothing to fire on for pure-SwiftUI screens — Pages shows zero rows.

```
struct CreateImageView: View {
    var body: some View {
        VStack {
            // your UI
        }
        .crossdeckScreen("Create Image")
    }
}
```

### .crossdeckTap("Name") on every important CTA

Populates **Top Actions** on the page-detail view and renders "Tapped 'Name'" on the per-person journey. Without it, taps still fire but with an empty label — the journey reads the useless "Clicked an element."

```
Button("Subscribe") {
    presentPaywall()
}
.crossdeckTap("Subscribe")
```

Two lines per screen, two lines per CTA. Every other dashboard surface works the moment `start` succeeds.

## Step 6 · Identify your user

Until you call `identify(...)`, every event carries an anonymous ID minted on first launch and persisted to UserDefaults. The moment you know who the user is — after sign-in — call `identify` with your app's user ID. The SDK rotates the anonymous events onto the identified person automatically.

```
MyApp.cd?.identify(userId: "user_847")

// When the user signs out (sync — safe in a non-async handler).
// reset() is async; use resetSync() here, or wrap: Task { await MyApp.cd?.reset() }
MyApp.cd?.resetSync()
```

The `userId` is whatever your app uses — the database primary key, the Firebase UID, the iCloud user record name. Stable for the lifetime of the account.

Want the People feed to show real names instead of `anon_xxx`? `identify` also accepts optional `email` and `traits` — see [Identify users](https://cross-deck.com/docs/identify-users/) for the full signature.

## Step 7 · Verify the heartbeat

Build and run your app in the simulator or on a device. The SDK fires its boot heartbeat the moment `start` returns. Open [Developers → Heartbeat](https://app.cross-deck.com/developers/heartbeat) in the dashboard to confirm.

The page shows a per-app **SDK signal audit**:

- **Last seen** — should show "a few seconds ago" after you launch the app.

- **SDK version** — the version string the SDK stamped on its events. Confirms which build is live.

- **Recent events** — live tail of the last events received. You should see `session.started` and (if you wired the SwiftUI bolt-ons) `screen.viewed` within a few seconds.

End-to-end latency is 3–8 seconds.

The SDK batches events (default flush at 20 events or 5 seconds) → ingest writes to Firestore → the dashboard's onSnapshot listener fans the update out. If you don't see anything after 15 seconds, jump to [Troubleshooting](#troubleshooting).

### If nothing shows up after 30 seconds

Symptom Most likely cause Fix

Xcode console: CrossdeckError.invalidKeyPrefix
environment doesn't match the key (test key + .production , or live key + .sandbox ).
Set environment: .sandbox for cd_pub_test_… keys; .production for cd_pub_live_… . Use #if DEBUG to swap both together.

Xcode console: CrossdeckError.invalidAppId
The publishable key is for a different project than the appId.
Re-copy both values from the same app card on the API page.

No console errors, but Heartbeat shows nothing
You're on a simulator with no network, or the device is on a network where api.cross-deck.com is blocked.
Check the simulator/device has network access. Test by opening Safari to api.cross-deck.com ; you should see a 404 page (the root has no route, but DNS resolves).

Heartbeat shows events but no screen.viewed entries
You haven't added .crossdeckScreen("Name") modifiers to your SwiftUI views.
Add the modifier to every destination view (Step 5). The Pages dashboard reads from these.

Crash on launch: UninitializedPropertyAccessException
You used let cd: Crossdeck non-optional and start threw before initialisation completed.
Use let cd: Crossdeck? (Optional) and the do / catch pattern in Step 4. The host app should degrade silently, not crash.

## What's next

Heartbeat green? The SDK is live. From here:

- [Identify users](https://cross-deck.com/docs/identify-users/) — pass traits (email, plan, signup date) so the People feed renders real names instead of `anon_xxx`.

- [Track custom events](https://cross-deck.com/docs/track-events/) — `cd.track("paywall_seen", properties: ["variant": "annual"])` for the conversion events that matter to you.

- [Entitlements & gating](https://cross-deck.com/docs/entitlements/) — gate pro features with one line: `if cd.isEntitled("pro") { … }`.

- [Connect App Store](https://cross-deck.com/docs/connect-apple/) — start verifying StoreKit purchases and computing MRR from App Store subscriptions.

- [Apple in-app purchase: bank-grade attribution](https://cross-deck.com/docs/swift-sdk/#apple-attribution) — if your app makes StoreKit purchases, add one line at every purchase site (`.appAccountToken(Crossdeck.appAccountTokenForCurrentIdentity())`) so each subscription chain stays bound to the correct app user across `identify()`, account merges, and SSO upgrades. Required reading before you wire up StoreKit.

- [Observing App Review sessions](https://cross-deck.com/docs/guides/app-review-sessions/) — once your build is in Apple’s review pipeline, its sessions appear in Live and People like any anonymous visitor. Read a rejection beside the session that produced it.

- [Full Swift SDK reference](https://cross-deck.com/docs/swift-sdk/) — every method, every option, every event the SDK fires automatically, plus the SwiftUI pattern matrix for NavigationStack / .sheet / TabView.
