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:
- File → Add Package Dependencies
- Enter:
https://github.com/joryio/joryio-ios.git - 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
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
}
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$timezonecountry(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:
- Go to Settings → Apps → [Your App]
- Navigate to Push Notifications tab
- Upload your APNS certificate (.p12) or auth key (.p8)
- Enter your Team ID and Key ID (for .p8)
- 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()
Push registration and consent
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:
| Content | What it is | How it renders |
|---|---|---|
| Native | Structured data - headline, body, image, buttons | Real UIKit views, using your app's tint colour, Dynamic Type, dark mode and VoiceOver. No web view. |
| HTML | Author-supplied markup, CSS and JavaScript | A 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 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.
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 packaged | class the SDK looks for | |
|---|---|---|
| SwiftPM | JoryioUI is its own target | JoryioUI.DefaultInAppMessagePresenter |
| CocoaPods | Joryio/UI is a subspec, and subspecs share the pod's module | Joryio.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:
- Modal - Center of screen with backdrop
- Banner - Top of screen
- Slide-Up - Small notification from the bottom
- Full-Screen - Takeover message
- 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()andwipeData().
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 collection | yes - track, identify and setAttributes all become no-ops |
| survives a restart | yes - the flag is stored on the device and read before anything is collected |
| drops what is already queued | yes - queued events and un-acked attribute writes are discarded, not delivered later |
| tells the server | yes - one final $tracking_opted_out profile attribute, best-effort, sent while sending is still permitted |
| deletes stored data | no - 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
- Check SDK key is correct:
jry_sdk_ios_* - Enable debug logging:
enableDebug: true - Check console logs for errors
- Verify network connectivity
- Call
flush()to send immediately
Push Not Working
- Verify APNS certificate is uploaded in dashboard
- Check device token is registered
- Ensure proper entitlements in Xcode
- Test in proper environment (dev vs production)
Build Errors
- Ensure iOS 14.0+ deployment target
- Clean build folder: Cmd+Shift+K
- 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
- Android SDK - Integrate Android SDK
- Push Notifications Guide - Learn about push campaigns
- In-App Campaigns Guide - Create in-app messages
- Custom Events - Track custom events
- E-Commerce API - Server-side e-commerce integration