Skip to main content

iOS SDK Integration

Native iOS SDK for tracking events, managing user sessions, sending push notifications, and displaying in-app messages.

Features

  • Lightweight - Minimal footprint
  • Fast - Optimized for performance
  • Offline Support - SQLite-based event queue
  • Auto-Retry - Exponential backoff on failures
  • Batching - Efficient event batching (50 events / 5s)
  • In-App Messaging - native and HTML messages, 5 types, with frequency capping
  • Push Notifications - Apple Push Notification Service (APNS)
  • Privacy-First - GDPR compliant, respects user consent
  • iOS 14+ - Support for modern iOS versions

Requirements

  • iOS 14.0+
  • Xcode 15.0+
  • Swift 5.9+

Installation

Swift Package Manager

The SDK is distributed as a Swift package. Add the following to your Package.swift:

dependencies: [
.package(url: "https://github.com/joryio/joryio-ios.git", from: "1.0.0")
]

The package provides two products. Link whichever your app needs:

.target(
name: "YourApp",
dependencies: [
.product(name: "Joryio", package: "joryio-ios"), // tracking, identity, push
.product(name: "JoryioUI", package: "joryio-ios"), // + in-app message display
]
)

Or in Xcode:

  1. File → Add Package Dependencies
  2. Enter: https://github.com/joryio/joryio-ios.git
  3. Select version, then choose the products to add to your target

CocoaPods

pod 'Joryio/UI'   # everything, including in-app display
# pod 'Joryio' # tracking, identity and push only - no WebKit linked
Which one?

JoryioUI depends on Joryio, so linking the UI product gets you both.

Take Joryio alone when your app has no use for in-app messages, or when a security review objects to a web view being linkable at all. In-app messages will simply not display; everything else works unchanged. Note the CocoaPods default is the UI-free Joryio - pod 'Joryio' will not display in-app messages until you switch it to pod 'Joryio/UI'.

Quick Start

1. Initialize the SDK

In your AppDelegate.swift:

import Joryio

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {

// Initialize with your SDK key
Joryio.shared.initialize(
sdkKey: "jry_sdk_ios_YOUR_SDK_KEY",
apiHost: "api-eu1.joryio.com"
)

return true
}
Finding Your SDK Key

Find your SDK key in the Joryio dashboard under Settings → Apps → [Your App] → SDK Keys.

2. Track Events

// Basic event
Joryio.shared.track("Button Tapped")

// Event with properties
Joryio.shared.track("Product Viewed", properties: [
"product_id": "abc123",
"product_name": "Wireless Headphones",
"price": 99.99,
"category": "Electronics"
])

// Screen view
Joryio.shared.trackScreen("ProductDetail", properties: [
"product_id": "abc123"
])

3. Identify Users

// Identify a user
Joryio.shared.identify("user-123")
Joryio.shared.setAttributes([
"email": "user@example.com",
"name": "John Doe",
"plan": "premium"
])

// Set attributes later
Joryio.shared.setAttribute("last_purchase", value: Date())
Joryio.shared.incrementAttribute("lifetime_value", by: 99.99)

// On logout
Joryio.shared.reset()

Automatic Session Data

The iOS SDK enriches Session Start events with device and environment data:

  • $device_id
  • $platform (ios)
  • $model
  • $os_name / $os_version
  • $app_version / $build_number
  • $bundle_id
  • $screen_width / $screen_height
  • $locale
  • $language / $languages
  • $timezone
  • country (ISO-3166-1 alpha-2, derived from IP at session start)

Push Notifications

Enable push notifications to send targeted messages via Apple Push Notification Service (APNS).

1. Setup in AppDelegate

import Joryio

class AppDelegate: UIResponder, UIApplicationDelegate {

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Initialize SDK
Joryio.shared.initialize(
sdkKey: "jry_sdk_ios_YOUR_KEY",
apiHost: "api-eu1.joryio.com"
)

// Request push permissions
Task {
let granted = await Joryio.shared.requestPushPermissions()
if granted {
print("Push notifications enabled")
}
}

return true
}

// Handle device token registration
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
Joryio.shared.didRegisterForRemoteNotifications(deviceToken: deviceToken)
}

// Handle registration failure
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
Joryio.shared.didFailToRegisterForRemoteNotifications(error: error)
}

// Handle received push notification
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
Joryio.shared.didReceiveRemoteNotification(userInfo, completionHandler: completionHandler)
}
}

2. Configure APNS in Dashboard

To send push notifications, configure your APNS credentials:

  1. Go to Settings → Apps → [Your App]
  2. Navigate to Push Notifications tab
  3. Upload your APNS certificate (.p12) or auth key (.p8)
  4. Enter your Team ID and Key ID (for .p8)
  5. Select environment (Development/Production)

3. Push Features

// Check if push is enabled
let isEnabled = await Joryio.shared.isPushEnabled()

// Get device token
if let token = Joryio.shared.getDeviceToken() {
print("Device token: \(token)")
}

// Badge management
Joryio.shared.updateBadgeCount(5)
Joryio.shared.clearBadge()

// Unregister from push
Joryio.shared.unregisterFromPushNotifications()

The SDK registers for remote notifications whether or not the user grants the notification permission. On iOS the two are separate: registering yields a device token without consent, and that token can only ever deliver background (silent) pushes - it cannot display anything the user has not authorised.

This is what lets an in-app message reach a user who declined notifications, and it means a token already exists if they later enable notifications in Settings. Airship and OneSignal behave the same way. Disclose it in your privacy policy.

In-App Messaging

Display targeted in-app messages to users based on their behavior and attributes.

Delivery tokens

When the backend serves an eligible campaign it also issues a short-lived signed delivery token for it. The SDK echoes that token back when it reports an impression, click or dismissal, and the server verifies the signature before recording anything.

You do not have to do anything - the SDK handles this for you. It is documented because it changes what happens to a client that does not send one:

POST /v1/in-app/track   (no deliveryToken)
{ "success": false, "error": "A delivery token is required" }

The token is what makes an impression trustworthy: without it, anyone holding the SDK key - which ships inside every app and page - could report impressions and clicks for a campaign that was never shown, and your reporting would count them.

If in-app impressions stop being recorded, check that the app is built against a current version of the SDK: a build made before delivery tokens existed sends no token, and the server will reject its impressions.

Automatic Campaign Sync

The SDK automatically syncs campaigns from the server when:

  • The app enters the foreground
  • A push notification is received

Syncs are rate-limited to at most one per sync interval; call syncInAppCampaigns() to sync at other moments (for example after identifying a user or updating attributes).

Manual Campaign Control

// Manually sync campaigns from server
await Joryio.shared.syncInAppCampaigns()

// Manually trigger campaign evaluation
await Joryio.shared.evaluateInAppCampaigns()

// Reset displayed campaigns (for testing)
Joryio.shared.resetDisplayedCampaigns()

Two kinds of message content

A campaign arrives as one of two content shapes, and the SDK renders each one differently:

ContentWhat it isHow it renders
NativeStructured data - headline, body, image, buttonsReal UIKit views, using your app's tint colour, Dynamic Type, dark mode and VoiceOver. No web view.
HTMLAuthor-supplied markup, CSS and JavaScriptA WKWebView inside the message.

Allowing HTML messages

HTML messages are turned off by default. An HTML message runs author-supplied JavaScript inside your app, so turning it on is a decision your app team makes - not something switched on from a marketing dashboard:

let config = JoryioConfig(
inApp: InAppConfig(allowHtmlJsInAppMessages: true) // default: false
)

Joryio.shared.initialize(
sdkKey: "jry_sdk_ios_YOUR_KEY",
apiHost: "api-eu1.joryio.com",
config: config
)

Leaving it off does not disable in-app messaging. Native messages still display, because they are data rendered by the SDK's own views with no interpreter involved. HTML campaigns are skipped and logged, so an app that has not opted in shows no message rather than a broken one.

If your security policy forbids running authored HTML in-process, leave this off and author your campaigns as native messages.

tvOS

tvOS has no web view at all, so HTML messages never display there regardless of this setting. Author native messages for tvOS targets.

Rendering messages yourself

The SDK draws native messages for you, but you can take over completely - the same escape hatch other vendors call a custom view factory. Conform to InAppMessagePresenter and assign it:

Joryio.shared.inAppPresenter = MyPresenter()

Auto-discovery only runs when inAppPresenter is nil, so yours replaces the built-in renderer rather than fighting it. Set it before or after initialize.

Your presenter receives the campaign with content already resolved - the Liquid is rendered server-side, and native fields arrive as plain text. Track what you display with Joryio.shared.trackInAppImpression(campaignId, action:) so analytics still see impressions and clicks.

The class name differs by package manager

You never configure this, but it is worth knowing when something does not display. The SDK finds its built-in renderer by name at runtime, and the module that name lives in depends on how you integrated:

how it is packagedclass the SDK looks for
SwiftPMJoryioUI is its own targetJoryioUI.DefaultInAppMessagePresenter
CocoaPodsJoryio/UI is a subspec, and subspecs share the pod's moduleJoryio.DefaultInAppMessagePresenter

Both names are tried. If no in-app message ever appears, look for the SDK's warning "No in-app presenter found" - that means the UI product is not linked (pod 'Joryio/UI', or the JoryioUI product under SwiftPM), not that the campaign failed to arrive. Those two look identical from the outside.

Your own presenter needs none of this: assign it and discovery never runs.

Message Types

The SDK supports 5 message types:

  1. Modal - Center of screen with backdrop
  2. Banner - Top of screen
  3. Slide-Up - Small notification from the bottom
  4. Full-Screen - Takeover message
  5. Custom - Your app decides placement

Frequency Capping

Messages respect workspace-level touching rules and campaign-level frequency caps:

  • Maximum impressions per time window
  • Minimum delay between messages
  • Per-campaign frequency limits

Attribute delivery

setAttributes is durable. A write is queued until the server acknowledges it, so an attribute set while the device is offline is delivered when connectivity returns rather than dropped.

  • Retried on the next setAttributes, on foreground, and before an in-app sync.
  • Batched - a burst of calls becomes one request (800ms window). A single write still goes out promptly.
  • Persisted in the platform's encrypted store (iOS Keychain, Android EncryptedSharedPreferences), so a write survives the process being killed. Cleared the moment the server acks, and purged by optOut() and wipeData().

Attributes are also used for in-app targeting. The server profile is authoritative: only writes the server has not yet acknowledged can override it, which is what keeps two devices belonging to the same contact from disagreeing about who that contact is.

Configuration Options

Customize SDK behavior with JoryioConfig:

JoryioConfig(
// User Identification
userId: String?, // Initialize with known user ID
anonymousId: String?, // Custom anonymous ID

// Batching & Performance
batchSize: Int, // Default: 50
flushInterval: TimeInterval, // Default: 5.0 seconds
sendImmediately: Bool, // Default: false
maxQueueSize: Int, // Default: 1000

// Session Management
sessionTimeout: TimeInterval, // Default: 1800 (30 minutes)
trackSessionStart: Bool, // Default: true

// Storage
persistQueue: Bool, // Default: true

// Network & Retry
maxRetries: Int, // Default: 3
retryBackoffMs: Double, // Default: 1000.0
requestTimeout: TimeInterval, // Default: 10.0

// Privacy & GDPR
respectDoNotTrack: Bool, // Default: true
optOut: Bool, // Default: false
trackingConsent: TrackingConsent, // Default: .granted

// Debugging
enableDebug: Bool, // Default: false
logLevel: LogLevel // Default: .error
)

Advanced Features

User Attributes Management

// Set multiple attributes
Joryio.shared.setAttributes([
"age": 28,
"city": "San Francisco",
"premium": true
])

// Set single attribute
Joryio.shared.setAttribute("language", value: "en")

// Increment numeric attribute
Joryio.shared.incrementAttribute("page_views", by: 1)
Joryio.shared.incrementAttribute("total_spent", by: 29.99)

// Remove attribute
Joryio.shared.unsetAttribute("temporary_flag")

Privacy Controls

// Stop collecting. PERSISTED - survives an app restart.
Joryio.shared.optOut()

// Opt back in
Joryio.shared.optIn()

// Check opt-out status
if Joryio.shared.isUserOptedOut() {
print("User has opted out")
}

// Delete everything the SDK stored on this device.
// SEPARATE from optOut(): "stop collecting" and "delete what you have" are
// different requests. This is the one an erasure request needs. It does NOT
// opt the user out - call optOut() as well if that is also intended.
Joryio.shared.wipeData()

// Get identity info
let (userId, anonymousId) = Joryio.shared.getIdentity()
print("User: \(userId ?? "anonymous"), Anonymous ID: \(anonymousId)")

What optOut() does, precisely:

stops collectionyes - track, identify and setAttributes all become no-ops
survives a restartyes - the flag is stored on the device and read before anything is collected
drops what is already queuedyes - queued events and un-acked attribute writes are discarded, not delivered later
tells the serveryes - one final $tracking_opted_out profile attribute, best-effort, sent while sending is still permitted
deletes stored datano - use wipeData()

The $tracking_opted_out attribute is a record, not enforcement: it lands on the profile so campaigns can exclude on it. Server-side suppression is a separate setting.

Manual Queue Flushing

// Flush immediately (e.g., before app termination)
Joryio.shared.flush()

// Check queue size
let queueSize = Joryio.shared.getQueueSize()
print("Pending events: \(queueSize)")

Best Practices

1. Track Meaningful Events

Focus on events that matter for your business:

// Good: Specific, actionable events
Joryio.shared.track("Trial Started", properties: ["plan": "premium"])
Joryio.shared.track("Feature Used", properties: ["feature": "export"])

// Avoid: Overly generic events
Joryio.shared.track("Button Tapped") // Too generic

2. Handle User Lifecycle

// On login
func handleLogin(userId: String, userInfo: UserInfo) {
Joryio.shared.identify(userId)
Joryio.shared.setAttributes([
"email": userInfo.email,
"name": userInfo.name
])
}

// On logout
func handleLogout() {
Joryio.shared.reset()
}

// On signup
func handleSignup(userId: String, userInfo: UserInfo) {
Joryio.shared.alias(userId)
Joryio.shared.identify(userId)
Joryio.shared.setAttributes(userInfo.attributes)
}

3. Flush on Critical Events

override func applicationWillTerminate(_ application: UIApplication) {
Joryio.shared.flush()
}

Troubleshooting

Events Not Appearing

  1. Check SDK key is correct: jry_sdk_ios_*
  2. Enable debug logging: enableDebug: true
  3. Check console logs for errors
  4. Verify network connectivity
  5. Call flush() to send immediately

Push Not Working

  1. Verify APNS certificate is uploaded in dashboard
  2. Check device token is registered
  3. Ensure proper entitlements in Xcode
  4. Test in proper environment (dev vs production)

Build Errors

  1. Ensure iOS 14.0+ deployment target
  2. Clean build folder: Cmd+Shift+K
  3. Update Swift Package dependencies

E-Commerce Tracking

The iOS SDK includes a built-in e-commerce tracker for product, cart, checkout, and order events. See the cross-SDK E-Commerce Tracking guide for the full API with Swift examples.

Next Steps