iOS SDK quickstart
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.
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 — 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.
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. |
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 and select your project from the workspace switcher (top-left).
- From the left sidebar, click Developers, then API keys. The direct path is
/dashboard/developers/api-keys/. - 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
appIdat 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.
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/VistaApps-za/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
Crossdecklibrary 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
}
}
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:
- 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.startedand (if you wired the SwiftUI bolt-ons)screen.viewedwithin a few 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
| 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 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:
- Identify users — pass traits (email, plan, signup date) so the People feed renders real names instead of
anon_xxx. - Track custom events —
cd.track("paywall_seen", properties: ["variant": "annual"])for the conversion events that matter to you. - Entitlements & gating — gate pro features with one line:
if cd.isEntitled("pro") { … }. - Connect App Store — start verifying StoreKit purchases and computing MRR from App Store subscriptions.
- Apple in-app purchase: bank-grade 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 acrossidentify(), account merges, and SSO upgrades. Required reading before you wire up StoreKit. - Full Swift SDK reference — every method, every option, every event the SDK fires automatically, plus the SwiftUI pattern matrix for NavigationStack / .sheet / TabView.