iOS SDK
Install and integrate the LinkTrail iOS SDK — module LinkTrailSDK, iOS 15+, via Swift Package Manager or CocoaPods — with configure, onLink and consent.
Binary XCFramework · module LinkTrailSDK · entry point LinkTrail · iOS 15+, Swift 5.9 / Xcode 15+ · Swift Package Manager or CocoaPods.
Before this works, set up three things in the dashboard. Skip one and links fail silently — they open the browser instead of your app. Full flow: Quickstart.
- 1Register the app — Apps → Register app, with your Apple Team ID and bundle ID.
- 2Add a link domain — Domains. Or use your free
*.linktrail.iosubdomain. - 3Create an API key — Settings → API Keys. That is the
lt_live_…value below.
Install
Swift Package Manager
In Xcode: File → Add Package Dependencies… → paste the repo URL → pick a version. Or in a Package.swift:
.package(url: "https://github.com/linktrail-io/ios-sdk.git", from: "0.0.11")CocoaPods
pod 'LinkTrailSDK', '~> 0.0.11'Then run pod install and open the generated .xcworkspace.
Integrate
import LinkTrailSDK
// At launch (SwiftUI App.init or AppDelegate). The API key is required — configure throws.
try LinkTrail.configure(apiKey: "lt_live_...")
// One hook — fires for deferred (first-launch) AND re-engagement links:
LinkTrail.shared?.onLink { link, source in
router.route(to: link.path, customData: link.customData)
}
LinkTrail.shared?.onError { error in /* e.g. LinkTrailError.invalidAPIKey */ }Consent
Consent gating is on by default (requireConsent: true) and follows a "links work, tracking waits" model: until the end user consents, deep links still route — deferred and re-engagement links reach their destination via onLink — but no attribution is recorded. When the user accepts your consent prompt, grant it:
// Attributes the install and flushes queued events — without re-routing
// a user who has already been sent to their screen:
LinkTrail.shared?.setConsent(true)
// Revoke at any time (clears the queued events):
LinkTrail.shared?.setConsent(false)
// To attribute at init with no gate, opt out:
try LinkTrail.configure(apiKey: "lt_live_...",
options: LinkTrailOptions(requireConsent: false))Forward incoming links — SwiftUI:
.onOpenURL { LinkTrail.shared?.handleDeepLink($0) }
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
if let url = activity.webpageURL { LinkTrail.shared?.handleDeepLink(url) }
}Or UIKit, from your SceneDelegate:
// Universal Links (app already running or cold-started from a link):
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if let url = userActivity.webpageURL { LinkTrail.shared?.handleDeepLink(url) }
}
// Custom schemes (yourapp://…):
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
URLContexts.forEach { LinkTrail.shared?.handleDeepLink($0.url) }
}handleDeepLink returns false for URLs that aren't LinkTrail links, so you can fall through to your own routing. Every callback API also has an async throws twin (e.g. try await LinkTrail.shared?.handleDeepLink(url) returns the resolved link). Callbacks are delivered on the main thread.
More
// Custom + revenue events:
LinkTrail.shared?.trackEvent(name: "purchase", value: 59.99, currency: "USD")
// Cached results:
let attribution = LinkTrail.shared?.lastAttribution
let lastLink = LinkTrail.shared?.lastDeepLink
// Attribution hook (fires when an install is attributed):
LinkTrail.shared?.onAttribution { attribution in /* … */ }
// Defer the install call entirely (send it yourself later):
let lt = try LinkTrail.configure(apiKey: "lt_live_...",
options: LinkTrailOptions(autoTrackInstall: false))
lt.trackInstall()
// ATT / SKAdNetwork:
await LinkTrail.shared?.requestTrackingAuthorization()
LinkTrail.shared?.registerForSKAdAttribution()
LinkTrail.shared?.updateConversionValue(42, coarseValue: .medium)Calling requestTrackingAuthorization() requires NSUserTrackingUsageDescription in your Info.plist — without it the ATT prompt never appears and authorization is denied.
Deferred attribution & the click token
Android gets deferred attribution from the Play Install Referrer, which the store passes through automatically. iOS has no equivalent, so LinkTrail carries a click token on the clipboard: the tapped link leaves it there and the SDK recovers it on first launch. clickTokenSource controls how that read happens.
| clickTokenSource | Behaviour | Trade-off |
|---|---|---|
| .pasteButton (default) | The token is read only when the user taps a paste control you render — LinkTrailPasteButton, shipped with the SDK. | No system alert, but it needs UI (iOS 16+) and autoTrackInstall: false. |
| .automatic | The SDK reads the clipboard itself at install. | No UI, but iOS shows the “Allow Paste” alert on first launch. |
| .none | The clipboard is never touched, by any path — trackInstall(clickToken:) ignores a token handed to it. | No alert and no UI; deferred matching falls back to probabilistic only. |
The default is .pasteButton, so deferred attribution on iOS records nothing until you render a paste control and set autoTrackInstall: false. If deferred installs arrive unattributed on iOS with everything else wired correctly, this is almost always why. Use .automatic to accept the Allow Paste alert instead, or .none to stay off the clipboard entirely.
import LinkTrailSDK
import SwiftUI
// The install waits for the tap, so don't auto-track it.
try LinkTrail.configure(
apiKey: "lt_live_...",
options: LinkTrailOptions(autoTrackInstall: false,
clickTokenSource: .pasteButton)
)
// On your first-launch screen, render the SDK's paste control. The tap is the
// consent, so there is no "Allow Paste" alert: the SDK reads the token, fires
// the install, and onLink routes the recovered destination.
if #available(iOS 16.0, *) {
LinkTrailPasteButton()
}LinkTrailPasteButton wraps Apple’s UIPasteControl, which is iOS 16+. On iOS 15 there is no paste control at all, so use .automatic or .none there. React Native and Flutter ship the same component — see LinkTrailPasteButton on those pages.
Rolling your own UIPasteControl instead? Its target must be a UIResponder that sets pasteConfiguration and overrides paste(itemProviders:) — without the pasteConfiguration the control stays disabled and never delivers. Hand the pasted string to LinkTrail.shared?.trackInstall(clickToken:).
Options & defaults
| LinkTrailOptions field | Default | Notes |
|---|---|---|
| requireConsent | true | Consent gate: links route immediately, attribution waits for setConsent(true) |
| autoTrackInstall | true | false = defer the install call entirely; call trackInstall() yourself |
| logEnabled | false | Master switch for SDK logging |
| logLevel | .info | .debug · .info · .warning · .error · .none |
| requestTimeout | 15 s | Per-request network timeout |
| retryPolicy | .default | 3 attempts, 0.5 s base / 8 s max backoff; .disabled turns retries off |
| linkDomains | [] (all hosts) | See the warning under Deep-link setup before setting this |
| clickTokenSource | .pasteButton | How the deferred click token is read on first launch (iOS paste control) |
API surface
| API | Purpose |
|---|---|
| configure(apiKey:options:) | Initialize; links route immediately, attribution waits for consent |
| setConsent(granted) | Grant or revoke the consent gate (see Consent above) |
| onLink { link, source } | Deep link delivered (deferred + re-engagement) |
| onAttribution { attribution } | Install attribution result |
| onError { error } | Failures (invalid key, network…) |
| handleDeepLink(url) | Forward Universal Links / custom schemes |
| trackEvent(name:value:currency:) | Custom + revenue events |
| trackInstall(force:) | Manual install track (with autoTrackInstall: false) |
| lastAttribution / lastDeepLink | Cached last results |
| requestTrackingAuthorization() | ATT prompt wrapper |
| registerForSKAdAttribution() / updateConversionValue(_:coarseValue:) | SKAdNetwork |
Deep-link setup
Add the Associated Domains capability with applinks:yourapp.linktrail.io. For a custom scheme, add it under CFBundleURLTypes in Info.plist.
The AASA file is generated and hosted by LinkTrail — it's built from the Team ID + bundle ID you registered in the dashboard. Nothing to upload. If you set linkDomains in LinkTrailOptions, list every link host: with a non-empty list, re-engagement opens only route for listed hosts (deferred install links still route, so the bug hides on fresh installs). Leave it empty to handle every parseable link.
App Store privacy
The SDK ships its own privacy manifest (PrivacyInfo.xcprivacy), so Xcode's privacy report includes it automatically. What it declares: a per-install device identifier (IDFV, with a UUID fallback) linked to the user, used for Analytics and App Functionality; no IDFA collection; no cross-company tracking (NSPrivacyTracking = false). In App Store Connect's App Privacy questionnaire, count LinkTrail as: Identifiers → Device ID, linked to the user, not used for tracking.
Example app: KickFlip — a SwiftUI storefront in example/ wired end to end. cd example && xcodegen generate && open KickFlipDemo.xcodeproj.