iOS SDK Setup Guide (Swift)
The Linkzly iOS SDK is a full mobile attribution and deep linking platform. It tracks installs, opens, and custom events; handles Universal Links and deferred d
iOS SDK Setup Guide (Swift)
The Linkzly iOS SDK is a full mobile attribution and deep linking platform. It tracks installs, opens, and custom events; handles Universal Links and deferred deep linking; captures affiliate attribution for server-to-server (S2S) postbacks; supports SKAdNetwork and ATT; integrates Firebase Cloud Messaging for push; and includes a dedicated gaming event pipeline.
The SDK has zero third-party dependencies, ships with a PrivacyInfo manifest, supports SwiftUI and UIKit, and is fully Objective-C bridged.
Prerequisites
Before integrating, set up your app in the Linkzly Console:
- Go to Dashboard > Apps and click "Register App".
- Enter your iOS Bundle ID and Team ID.
- Choose a verification method (Hosted is recommended for a quick start).
- Copy your SDK Key from the post-creation wizard, or from Manage App > Overview > SDK Configuration.
| Requirement | Minimum | Recommended |
|---|---|---|
| iOS | 12.0+ | 15.0+ |
| macOS | 10.14+ | 12.0+ |
| Xcode | 14.0+ | 15.0+ |
| Swift | 5.7+ | 5.9+ |
Your SDK key starts with slk_ and uniquely identifies one app — do not share keys between apps.
Installation
Swift Package Manager
In Xcode, choose File → Add Package Dependencies and enter:
https://github.com/Linkzly/linkzly-ios-package.git
Select a version (1.0.0 or later) and add the Linkzly product to your target. Or, in Package.swift:
dependencies: [
.package(url: "https://github.com/Linkzly/linkzly-ios-package.git", from: "1.0.0")
]
.target(name: "YourApp", dependencies: ["Linkzly"])
CocoaPods and Carthage support are planned but not yet available.
Platform Setup
Associated Domains (Universal Links)
Universal Links require an Associated Domains entitlement. In Xcode, select your target → Signing & Capabilities → + Capability → Associated Domains, then add:
applinks:{your-prefix}.linkz.ly
If you chose Hosted Verification in the console, Linkzly hosts your apple-app-site-association file automatically. For Custom Domain Verification, host the file at https://yourdomain.com/.well-known/apple-app-site-association:
{
"applinks": {
"apps": [],
"details": [
{ "appID": "TEAMID.com.yourcompany.yourapp", "paths": ["*"] }
]
}
}
Info.plist
Add a custom URL scheme (helpful for testing) and a tracking-permission description string for ATT:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array><string>yourapp</string></array>
</dict>
</array>
<key>NSUserTrackingUsageDescription</key>
<string>We use your data to provide personalized content and improve your app experience.</string>
Initialization
SwiftUI
import SwiftUI
import Linkzly
@main
struct YourApp: App {
init() {
LinkzlySDK.configure(
sdkKey: "slk_your_key_from_console",
environment: .production
)
// Optional: closure-based deep link handlers (recommended)
LinkzlySDK.onUniversalLink { url, attributionData in
// Fired immediately when a Universal Link is captured — use for fast navigation.
}
LinkzlySDK.onDeepLink { deepLinkData, url in
// Fired when the server returns enriched attribution data, including
// deferred deep links for fresh installs.
}
}
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
_ = LinkzlySDK.handleUniversalLink(url)
}
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in
_ = LinkzlySDK.handleUniversalLink(userActivity)
}
}
}
}
UIKit
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
LinkzlySDK.configure(sdkKey: "slk_your_key_from_console", environment: .production)
return true
}
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
return LinkzlySDK.handleUniversalLink(userActivity)
}
}
The SDK supports .development, .staging, and .production environments. Development mode enables verbose console logging.
Tracking Events
// Custom event
LinkzlySDK.trackEvent("purchase_completed", parameters: [
"product_id": "12345", "amount": 29.99, "currency": "USD"
])
// Purchase with completion handler
LinkzlySDK.trackPurchase(parameters: [
"product_id": "12345", "amount": 29.99, "currency": "USD",
"transaction_id": "txn_abc123"
]) { result in
switch result {
case .success(let response): print("Tracked: \(response.eventId ?? "")")
case .failure(let error): print("Error: \(error)")
}
}
// Install / open (called automatically on launch; use the explicit form when you
// need the deferred-deep-link callback)
LinkzlySDK.trackInstall { result in /* ... */ }
LinkzlySDK.trackOpen { result in /* ... */ }
// Batch
LinkzlySDK.trackEventBatch([
["eventName": "view_item", "parameters": ["item_id": "123"]],
["eventName": "add_to_cart","parameters": ["item_id": "123", "quantity": 1]]
]) { success, error in /* ... */ }
Event queue
let pending = LinkzlySDK.getPendingEventCount()
LinkzlySDK.flushEvents { success, error in /* ... */ }
User Identity
LinkzlySDK.setUserID("user_12345")
let userId = LinkzlySDK.getUserID()
// Persistent visitor ID (auto-generated UUID)
let visitorId = LinkzlySDK.getVisitorID()
LinkzlySDK.resetVisitorID()
// Session control (optional — sessions are automatic by default)
LinkzlySDK.startSession()
LinkzlySDK.endSession()
Deep Linking and Universal Links
The SDK provides two handler methods, used together:
onUniversalLink fires immediately when a Universal Link URL is captured (before the server roundtrip) — ideal for fast navigation in installed apps.
LinkzlySDK.onUniversalLink { url, attributionData in
// url: the captured URL
// attributionData: query parameters from the URL
}
onDeepLink fires when the server returns enriched attribution data, including the smart link ID, click ID, and deferred deep links for fresh installs.
LinkzlySDK.onDeepLink { deepLinkData, url in
// deepLinkData.path, .smartLinkId, .clickId, .parameters
}
You can also observe NotificationCenter notifications: .linkzlyUniversalLinkReceived, .linkzlyDeepLinkDataReceived, .linkzlyAffiliateAttributionCaptured, and .linkzlyServerConfigReceived.
DeepLinkData exposes typed parameter getters:
class DeepLinkData {
let url: String?
let path: String?
let smartLinkId: String?
let clickId: String?
let parameters: [String: Any]
func getStringParameter(_ key: String) -> String?
func getNumberParameter(_ key: String) -> NSNumber?
func getBoolParameter(_ key: String) -> Bool
}
Affiliate Attribution
Capture an affiliate click from any incoming Universal Link, then read it back at checkout for S2S postbacks:
LinkzlySDK.onUniversalLink { url, _ in
LinkzlySDK.captureAffiliateAttribution(from: url)
}
Or in UIKit:
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if let url = userActivity.webpageURL {
LinkzlySDK.captureAffiliateAttribution(from: url)
}
return LinkzlySDK.handleUniversalLink(userActivity)
}
At conversion time:
if let clickId = LinkzlySDK.getAffiliateClickId() {
sendToServer(clickId: clickId, orderId: orderId, amount: amount)
}
if let attribution = LinkzlySDK.getAffiliateAttribution() {
// attribution.clickId, .network, .campaign
}
LinkzlySDK.hasAffiliateAttribution()
LinkzlySDK.clearAffiliateAttribution()
Attribution is stored in the Keychain (with UserDefaults fallback), expires automatically after 30 days, and is overwritten by newer affiliate clicks.
SKAdNetwork and ATT
Request tracking permission (iOS 14.5+)
import AppTrackingTransparency
LinkzlySDK.requestTrackingPermission { result in
if case .success(let status) = result {
// .authorized | .denied | .restricted | .notDetermined
}
}
if let idfa = LinkzlySDK.getIDFA() { /* ... */ }
if let attStatus = LinkzlySDK.getATTStatus() { /* "authorized" | ... */ }
IDFA is only collected when ATT status is authorized. IDFV is always available. Both identifiers are collected fresh on every event so consent changes are respected immediately.
Conversion values
LinkzlySDK.updateConversionValue(5)
LinkzlySDK.updateConversionValue(5) { success in /* ... */ }
if #available(iOS 16.1, *) {
LinkzlySDK.updateConversionValue(5, coarseValue: .high)
LinkzlySDK.updateConversionValue(5, coarseValue: .medium, lockWindow: true) { _ in }
}
Push Notifications
The SDK can subscribe the device to a Linkzly Firebase Cloud Messaging broadcast topic. Prerequisite: Firebase Messaging integrated in your app.
import FirebaseMessaging
import Linkzly
FirebaseApp.configure()
LinkzlySDK.configure(sdkKey: "slk_your_key_from_console", environment: .production)
let success = LinkzlySDK.initializePush() // returns false if Firebase is unavailable
LinkzlySDK.disablePush()
initializePush() uses runtime reflection — there is no Firebase dependency in the SDK itself, and it returns false safely when Firebase is missing. It works alongside other push providers (OneSignal, Braze, Airship) because it only subscribes to Linkzly-specific topics.
Privacy Controls
LinkzlySDK.setTrackingEnabled(false)
LinkzlySDK.isTrackingEnabled()
LinkzlySDK.setAdvertisingTrackingEnabled(false) // IDFA collection only
LinkzlySDK.isAdvertisingTrackingEnabled()
The SDK is GDPR-, CCPA-, App Store-, and ATT-compliant. The PrivacyInfo.xcprivacy manifest declares all collected data: device model, OS, app identifiers, locale, IDFA/IDFV (with consent), and ATT status.
Gaming Intelligence
A separate event pipeline for game telemetry with automatic batching, retry logic, optional HMAC request signing, and offline support.
Configure
let options = LinkzlyGamingOptions()
options.gameVersion = "1.2.0"
options.maxBatchSize = 50
options.flushIntervalMs = 10_000
options.debug = true
// options.signingSecret = "..." // required when HMAC is enabled in Game Settings
LinkzlySDK.configureGamingTracking(
apiKey: "your_gaming_api_key",
organizationId: "your_org_id",
gameId: "your_game_id",
environment: .production,
options: options
)
Identify player and track events
LinkzlySDK.identifyGamingPlayer("player_12345", traits: [
"level": 42, "vip_tier": "gold"
])
LinkzlySDK.trackGamingEvent("level_complete", data: [
"level": 5, "score": 12500, "stars": 3
])
// Bypass batching for critical events
LinkzlySDK.trackGamingEventImmediate("purchase", data: [
"item_id": "sword_of_fire", "price": 4.99, "currency": "USD"
])
Sessions and queue
LinkzlySDK.startGamingSession()
LinkzlySDK.endGamingSession()
LinkzlySDK.flushGamingEvents { success, error in /* ... */ }
LinkzlySDK.getGamingStatus { status in
// status.pendingEventCount, status.hasInflightBatch
}
LinkzlySDK.resetGamingTracking()
Gaming attribution
LinkzlySDK.setGamingAttribution(
clickId: "click_abc123",
deferredDeepLink: "https://yourgame.com/promo",
metadata: ["campaign": "summer_sale"]
)
LinkzlySDK.clearGamingAttribution()
HMAC request signing
HMAC signing is enabled by default for new games. Supply signingSecret in LinkzlyGamingOptions — the SDK computes the signature and attaches the X-Signature-256, X-Timestamp, and X-Nonce headers to every batch. The replay window is fixed at 300 seconds, so device clocks must be within 5 minutes of UTC. Your signing secret is in Game Settings → SDK Configuration, separate from the SDK Key. Never commit it to source — store it in Keychain or inject it via Xcode build settings.
Options reference
| Option | Default |
|---|---|
gameVersion |
"" |
maxBatchSize |
100 |
maxBatchBytes |
524288 (512 KB) |
flushIntervalMs |
5000 |
maxRetries |
3 |
retryDelayMs |
1000 |
maxQueueSize |
10000 |
sessionTimeoutMs |
1800000 (30 min) |
autoSessionTracking |
true |
signingSecret |
nil |
debug |
false |
Verify Your Setup
Run the app in .development and check the Xcode console:
🚀 LinkzlySDK configured with key: ...
📊 Tracking install event (first launch detected)
📤 Tracking event: test_event
🔗 Universal Link received: https://yourdomain.com/product?id=123
📥 Deep link data received: /product
Test deep links in the simulator:
xcrun simctl openurl booted "https://yourdomain.com/product?id=123"
xcrun simctl openurl booted "yourapp://product?id=123"
Troubleshooting
SDK not configured. Make sure LinkzlySDK.configure(sdkKey:environment:) runs before any other SDK call — ideally in App.init() or application(_:didFinishLaunchingWithOptions:).
Universal Links don't open the app. Verify the AASA file is served over HTTPS without redirects at /.well-known/apple-app-site-association, the appID matches your Team ID and Bundle ID, and the Associated Domains capability lists applinks:yourdomain.com. Hosted Verification skips the file step.
IDFA always nil. Confirm NSUserTrackingUsageDescription is set in Info.plist, you called LinkzlySDK.requestTrackingPermission, and the user authorized tracking.
initializePush() returns false. Configure Firebase (FirebaseApp.configure()) before calling initializePush(), and ensure APNs entitlements are set.
Gaming 401 with HMAC enabled. The signingSecret in LinkzlyGamingOptions must match Game Settings exactly, and the device clock must be within 5 minutes of UTC.
Was this helpful?
Help us improve our documentation