Crossdeck Docs
Dashboard

iOS SDK quickstart

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:

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.

Step 1 · Find your appId and publishable key

Crossdeck identifies your iOS app with two values:

ValueWhat it looks likeWhat 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

  1. Open the Crossdeck dashboard and select your project from the workspace switcher (top-left).
  2. From the left sidebar, click Developers, then API keys. The direct path is /dashboard/developers/api-keys/.
  3. 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

  1. Open your project in Xcode.
  2. In the Project navigator (left sidebar), click your project at the top — the entry with the blue Xcode icon (the .xcodeproj).
  3. 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).
  4. Along the top tab bar of the editor, click General (the leftmost tab).
  5. 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)

  1. In Xcode, click File → Add Package Dependencies…
  2. Paste the URL into the search field (top-right of the dialog):
    https://github.com/VistaApps-za/crossdeck-swift.git
  3. 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.
  4. Click Add Package. Xcode resolves the package and offers to add the Crossdeck library product to your app target — accept.

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

dependencies: [
    .package(url: "https://github.com/VistaApps-za/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
MyApp.cd?.reset()

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 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 in the dashboard to confirm.

The page shows a per-app SDK signal audit:

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.

If nothing shows up after 30 seconds

SymptomMost likely causeFix
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 API keys.
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: