iOS
Ensure you have completed the Prerequisites section before continuing.
Use the SDK version selector below to switch between documentation for the current release (1.4.x) and the previous 1.2.x line. Your selection is remembered across sections on this page.
Installation
The minimum supported version for iOS is 13.0.
To set the Minimum SDK target:
- Open your iOS project in Xcode
- Select your Target > General > Deployment Info > Ensure that the version is set to 13.0+
Swift Package Manager
- In your Xcode project, go to File > Add Package Dependencies…
- Enter the repository URL:
https://github.com/dashxhq/dashx-ios.git
- Add the following libraries to your app target:
DashX— required. Main SDK, shipped as a prebuilt XCFramework. Apollo is statically bundled inside and hidden behind@_implementationOnly import, so your app's own Apollo version (if any) won't collide with ours.DashXFirebase— required if you use push notifications (DashXAppDelegate). Kept as a source target so your app'sFirebaseMessagingis the instance DashX talks to at runtime.DashXNotificationServiceExtension— optional, add to a Notification Service Extension target to enable images on push banners, dynamic action buttons, and reliable delivered-tracking — see Notification Service Extension below.
Shared notification payload models (NavigationAction, ActionButton, DashXNotificationData, Constants) are re-exported through the DashX module — no separate DashXCore import needed.
The CocoaPods spec ships two subspecs: DashX/SDK (default) and DashX/NotificationServiceExtension. Both are vendored XCFrameworks — the same artifacts SPM consumers link — with Apollo statically baked into DashX.xcframework and the shared payload models compiled into each framework directly. The DashXFirebase helper is available only via Swift Package Manager. The pod is not published to the CocoaPods trunk — reference it from git by tag, e.g. pod 'DashX/SDK', :git => 'https://github.com/dashxhq/dashx-ios.git', :tag => '1.5.1'.
Configuration
DashX needs to be initialized as early as possible in your application's lifecycle, which can be done in the AppDelegate class within the application(_:didFinishLaunchingWithOptions:) method:
import DashX
// ...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
DashX.configure(
withPublicKey: "...", // required
baseURI: "...", // optional
targetEnvironment: "..." // optional
)
// ...
return true
}
Your Public Key is not sensitive, and can be stored in code or within Info.plist. For several environments, prefer xcconfig-driven values per build configuration rather than hard-coding secrets in Swift.
Multiple environments with xcconfig
If you use multiple Environments, a common pattern is:
- Add
.xcconfigfiles (for example under aConfig/group in your repo). Use a shared file per environment for real values, and debug / release files that#includethe shared file so Debug and Release stay in sync except where they differ. - Define build settings such as
DASHX_PUBLIC_KEY,DASHX_BASE_URI, andDASHX_TARGET_ENVIRONMENT. Standard.xcconfigsyntax cannot include//inside URLs; use a known workaround (e.g.https:/$()/api.example.com/graphql). - Wire configurations in Xcode: Project → Info → Configurations → assign each Debug / Release row to the correct
.xcconfigfor that environment. - Expose values to Swift via
Info.plistso they are visible toBundle.mainat runtime:
<key>DASHX_PUBLIC_KEY</key>
<string>$(DASHX_PUBLIC_KEY)</string>
<key>DASHX_BASE_URI</key>
<string>$(DASHX_BASE_URI)</string>
<key>DASHX_TARGET_ENVIRONMENT</key>
<string>$(DASHX_TARGET_ENVIRONMENT)</string>
- Read keys in code (same pattern as NSHipster: xcconfig in Swift):
enum Configuration {
enum Error: Swift.Error { case missingKey, invalidValue }
static func value<T>(for key: String) throws -> T where T: LosslessStringConvertible {
guard let object = Bundle.main.object(forInfoDictionaryKey: key) else {
throw Error.missingKey
}
switch object {
case let value as T: return value
case let string as String:
guard let value = T(string) else { fallthrough }
return value
default: throw Error.invalidValue
}
}
}
Then pass them into DashX.configure:
DashX.configure(
withPublicKey: try! Configuration.value(for: "DASHX_PUBLIC_KEY"),
baseURI: try? Configuration.value(for: "DASHX_BASE_URI"),
targetEnvironment: try? Configuration.value(for: "DASHX_TARGET_ENVIRONMENT")
)
For a complete, working app that wires xcconfig, Info.plist, and DashX.configure together, see the dashx-demo-ios repository (e.g. Config/, Utils/Configuration.swift, and AppDelegate).
Optional: lifecycle tracking
To automatically track app installed/updated/opened events and session length:
DashX.enableLifecycleTracking()
Optional: ad tracking
To request App Tracking Transparency permission and enable IDFA collection:
DashX.enableAdTracking()
This triggers the ATT permission prompt on iOS 14.5+. Make sure you have added the NSUserTrackingUsageDescription key to your Info.plist.
Permissions
dashx-ios SDK offers the following methods to manage notification permissions:
// Request permission to receive notifications
DashX.requestNotificationPermission { status in
// status is a UNAuthorizationStatus value
}
// Check current permission status without prompting
DashX.getNotificationPermissionStatus { status in
// status is a UNAuthorizationStatus value
}
On iOS 15+, the SDK requests .alert, .badge, .sound, and .timeSensitive. The .timeSensitive entitlement lets pushes with interruption-level: time-sensitive bypass Focus / Reduce Interruptions / Scheduled Summary — without it, those payloads silently downgrade to standard alerts on iOS 18 / 26 and stay subject to system filtering.
For .timeSensitive to actually be granted, your app target must have the Time Sensitive Notifications capability enabled in Xcode. Without it the option is silently dropped from the authorization request and the toggle never appears in Settings. See Receive push notifications → Time Sensitive Notifications for the setup steps.
Apple's requestAuthorization is a one-shot grant — calling it again on an already-.authorized user does not add new options to the existing grant. Users who granted permission under dashx-ios < 1.5.1 therefore don't have the time-sensitive entitlement.
To recover them, pass fallbackToSettings: true. When the user is already determined (granted or denied), the SDK opens the iOS notification-settings page so they can flip the Time Sensitive Notifications toggle manually:
DashX.requestNotificationPermission(fallbackToSettings: true) { status in
// status reflects the current authorization, not the toggle state
}
// async variant
let status = await DashX.requestNotificationPermission(fallbackToSettings: true)
Recommended pattern: surface this behind a "Notifications acting up?" affordance, or run it as a one-time prompt after the user upgrades to a build with dashx-ios 1.5.1+. Default false preserves the original ask-once behavior; existing call sites compile unchanged.
If you manage push tokens manually, you can set them directly:
// Set the APNS device token (typically in didRegisterForRemoteNotificationsWithDeviceToken)
DashX.setAPNSToken(to: deviceToken)
// Set the FCM token (if using Firebase Cloud Messaging)
DashX.setFCMToken(to: fcmToken)
Additionally the SDK tracks network information about the current network connection, containing bluetooth, carrier, cellular, and wifi.
Troubleshooting
As an optional step, you can set the log level for debugging your integration:
DashXLog.setLogLevel(to: .debug)
By default, the log level is set to .error. You can set it to one of: .debug (most logs), .info, .error or .off (no logs).
Usage
Most public methods have async/await variants for Swift concurrency. For example, try await DashX.identify(options:), try await DashX.subscribe(), try await DashX.fetchRecord(urn:), etc.
let prefs = try await DashX.fetchStoredPreferences()
let record = try await DashX.fetchRecord(urn: "blog/abc123")
let records = try await DashX.searchRecords(resource: "blog", limit: 10)
try await DashX.identify(options: ["email": "user@example.com"])
User Management
setIdentity)You can set uid without token and still use public features/resources (for example identify, track, and public content). The token is the DashX Identity Token: a JWT from your backend, signed with your workspace private key (see User management). Setting token grants access to your private resources in DashX—use it only for users who need that access, and treat it as sensitive. Pass nil for token when that access is not needed. uid and token are both optional (String?).
// User id only — public features/resources (add token for your private resources)
DashX.setIdentity(uid: "123", token: nil)
// Signed-in user + JWT for private resources
DashX.setIdentity(uid: "123", token: "your-identity-token")
// After token refresh, persist the new token (same uid)
DashX.setIdentity(uid: "123", token: "refreshed-identity-token")
// Send user attributes (Result-based or async)
DashX.identify(options: [
"uid": "123",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com"
]) { result in
switch result {
case .success:
break
case .failure(let error):
print(error.localizedDescription)
}
}
// Or with async/await:
// try await DashX.identify(options: ["email": "user@example.com"])
// Clear identity, generate new anonymous ID, and unsubscribe from push
DashX.reset()
Analytics
DashX.track("Button Clicked", withData: [
"label": "Click here",
"placement": "top"
])
Track screen views:
DashX.screen("HomeScreen", withData: [
"referrer": "deep_link"
])
Messaging
Push notifications require your app to be configured for Firebase Cloud Messaging or APNs.
DashX delivers pushes as user-visible APNs alert pushes — iOS renders the banner itself.
Notification Service Extension
Adding a Notification Service Extension (NSE) target is recommended for any app using DashX push. Without it, push notifications still display, but image attachments, dynamic action buttons, and background-delivered tracking will not work.
- In Xcode, File → New → Target… → Notification Service Extension.
- Delete the auto-generated
NotificationService.swiftfile that Xcode created inside the new target. - Add
DashXNotificationServiceExtension(SPM) orpod 'DashX/NotificationServiceExtension'to the NSE target. - Create a new
NotificationService.swiftinside the NSE target:
import DashXNotificationServiceExtension
final class NotificationService: DashXNotificationService {}
The module name is
DashXNotificationServiceExtension; the base class you subclass isDashXNotificationService. They're intentionally different names — Swift's module-interface verifier fails when a class name matches its module name.
- Add the same Info.plist keys to the NSE target that your main app uses for xcconfig-driven configuration (same names, same values). The NSE runs in its own process and cannot read the host app's bundle, so it needs its own copies:
<key>DASHX_BASE_URI</key>
<string>$(DASHX_BASE_URI)</string>
<key>DASHX_PUBLIC_KEY</key>
<string>$(DASHX_PUBLIC_KEY)</string>
<key>DASHX_TARGET_ENVIRONMENT</key>
<string>$(DASHX_TARGET_ENVIRONMENT)</string>
DASHX_TARGET_ENVIRONMENT is optional — include it only if your workspace uses environment-scoped broadcasts.
The NSE runs before iOS displays the notification and:
- Downloads the image from
dashx.imageand attaches it to the banner. - Registers a
UNNotificationCategorydynamically (SHA-256-hashed id) so long-press reveals the action buttons declared in the payload. - Falls back to the system default sound when the server didn't specify one.
- Fires a
trackMessage(status: DELIVERED)analytics call — even when your app isn't running.
If you use DashXAppDelegate (from the DashXFirebase module), notification delivered, clicked, and dismissed events are tracked automatically. No manual trackMessage() calls are needed.
// Subscribe for Push Notifications
// Important: identify must complete before calling subscribe,
// so the server knows which user to associate the device token with.
// Using async/await (recommended):
Task {
try await DashX.identify(options: ["uid": "user-123"])
try await DashX.subscribe()
}
// Using callbacks:
DashX.identify(options: ["uid": "user-123"]) { result in
if case .success = result {
DashX.subscribe()
}
}
// Unsubscribe from Push Notifications. The `Bool` carried in `.success` is
// `true` when the backend found and updated a matching subscribed contact,
// `false` when no match was found (e.g. the anonymous UID rotated since
// subscribe, the FCM token is stale, or the device was already unsubscribed).
// `false` is a non-error outcome — the device ends up unsubscribed either way.
DashX.unsubscribe { result in
switch result {
case .success(let didUnsubscribe):
print("did unsubscribe contact: \(didUnsubscribe)")
case .failure(let error):
print(error)
}
}
// Or, with async/await:
let didUnsubscribe = try await DashX.unsubscribe()
// Manage User Preferences (requires an identified user — setIdentity with uid + JWT when private resources require it)
DashX.fetchStoredPreferences { result in
switch result {
case .success(let preferences):
print(preferences) // [String: Any?]
case .failure(let error):
print(error.localizedDescription)
}
}
DashX.saveStoredPreferences(preferenceData: ["push": true, "email": false]) { result in
switch result {
case .success(let saved):
print(saved)
case .failure(let error):
print(error.localizedDescription)
}
}
CMS
DashX.fetchRecord(
urn: "email/welcome",
language: "en_US",
preview: true
) { result in
switch result {
case .success(let record):
print(record)
case .failure(let error):
print(error)
}
}
fetchRecord accepts the following optional arguments:
| Name | Type | Example |
|---|---|---|
preview | Bool | true |
language | String | "en_US" |
fields | [[String: Any]] | |
include | [[String: Any]] | |
exclude | [[String: Any]] |
DashX.searchRecords(
resource: "email",
language: "en_US",
order: [["created_at": "DESC"]],
limit: 10,
preview: true
) { result in
switch result {
case .success(let records):
print(records)
case .failure(let error):
print(error)
}
}
searchRecords accepts the following optional arguments:
| Name | Type | Example |
|---|---|---|
filter | [String: Any] | ["status": "published"] |
order | [[String: Any]] | [["created_at": "DESC"]] |
limit | Int | 10 |
page | Int | 1 |
preview | Bool | true |
language | String | "en_US" |
fields | [[String: Any]] | |
include | [[String: Any]] | |
exclude | [[String: Any]] |
Assets
DashX.uploadAsset(
fileURL: localFileURL,
resource: "users",
attribute: "avatar"
) { result in
switch result {
case .success(let asset):
print(asset)
case .failure(let error):
print(error)
}
}
Fetch an asset's status and URL:
DashX.fetchAsset(assetId: "asset-id") { result in
switch result {
case .success(let asset):
print(asset.url)
case .failure(let error):
print(error)
}
}
Asset uploads poll for completion with exponential backoff (2 s base, capped at 60 s). You can configure DashXClient.maxAssetPollRetries (default: 5) and DashXClient.assetPollBaseInterval (default: 2.0 s).
Error Handling
Completion-based methods return Result types. The SDK defines the following error types via DashXClientError:
| Error | Retryable | When |
|---|---|---|
noArgsInIdentify | No | identify() called without options |
notIdentified | No | Operation requires an identified user |
graphQLErrors | No | Server returned GraphQL errors |
networkError | Yes | Network-level failure |
assetIsNotReady | Yes | Asset still being processed |
assetIsNotUploaded | No | Asset upload failed |
customError | No | Other SDK error |
Use isRetryable to decide whether to retry:
DashX.fetchRecord(urn: "blog/abc") { result in
if case .failure(let error) = result,
let dashXError = error as? DashXClientError,
dashXError.isRetryable {
// retry later
}
}
All SDK errors are represented by DashXClientError, which conforms to LocalizedError. Each error provides errorDescription and recoverySuggestion for user-facing messages:
DashX.fetchStoredPreferences { result in
switch result {
case .success(let prefs):
print(prefs)
case .failure(let error):
print(error.localizedDescription)
if let dashxError = error as? DashXClientError {
print(dashxError.recoverySuggestion ?? "")
if dashxError.isRetryable {
// retry later
}
}
}
}
Offline Event Queue
Failed track() calls are automatically queued and retried with exponential backoff (2 s base, capped at 5 min, up to 10 retries, with jitter). The queue holds up to 1,000 events, is persisted via UserDefaults, and flushes automatically after configure() or when the network becomes available.
To manually flush:
DashX.flushEventQueue()
The sections above mirror the dashx-ios README (installation, offline queue, errors, asset polling, and async/await). Prefer this page for narrative context; the repo README stays focused on quick copy-paste snippets.