Competitive Research: iOS SDK Design of Deep-Linking / Attribution Platforms
Web research for the OptoLink native Swift SDK. Vendors: Branch, AppsFlyer, Adjust, Singular, Kochava, Tenjin + the post-Firebase-Dynamic-Links landscape and modern lightweight entrants. Researched 2026; sources inline. SDK facts verified against vendor source/headers where noted. Companion to android-sdk-competitive-research.md — iOS deltas are called out throughout.
1. Deferred deep linking on iOS: why it is structurally harder than Android
iOS has no install referrer. There is no OS-provided channel that hands the app the URL the user clicked before installing (Android's Play Install Referrer is the deterministic backbone there). Apple killed every viable workaround one by one: no referrer API, SFSafariViewController/App-Store-page cookie access gone, and since iOS 14.5 the ATT policy officially prohibits device fingerprinting for ad tracking. Every vendor's iOS DDL is therefore one of three hacks (or a cascade of them):
1.1 The three matching mechanisms used in practice
| # | Mechanism | How it works | Used by | Strength / weakness |
|---|---|---|---|---|
| 1 | Pasteboard (NativeLink™-style) — deterministic | The link's web landing page (Deepview) writes a token to the pasteboard via JS; SDK reads it on first launch and matches 100% | Branch NativeLink (v1.39.4+); AppsFlyer variant; old Adjust pasteboard matching | 100% match, but iOS 16+ shows the system "allow paste" prompt to every new user — a real onboarding UX cost vendors now document openly. https://help.branch.io/docs/nativelink-deferred-deep-linking |
| 2 | Server-side probabilistic matching (fingerprint) | Click page records IP, OS, device model, language, timezone; SDK sends the same signals at first open; server stitches click↔install in a match window (24h typical; Branch guarantees-flag only at 100% confidence) | Branch ("snapshot comparison"), AppsFlyer, Adjust, Singular, Kochava, all lightweight vendors | No prompt, no IDFA, survives ATT — but confidence degrades with CGNAT/iCloud Private Relay (shared exit IPs); Apple's ATT policy formally objects to fingerprinting for identification, so vendors frame it as "matching" for routing, not ad attribution. https://warplink.app/blog/deferred-deep-linking-ios |
| 3 | Deterministic click-ID via install-time session call | First session/attribution network call carries IDFV (vendor-scoped, resets when all vendor apps are deleted) and any stored click context; server returns matched link in the session/attribution response itself | Adjust (optimized DDL in /session response, legacy in /attribution), AppsFlyer conversion data + UDL, Kochava attribution response | Same-device reinstall matching is deterministic (IDFV) — but cross-app first-install matching still needs #1 or #2. https://dev.adjust.com/en/sdk/ios/features/deep-links/deferred/ |
Plus one Apple-only deterministic channel: Apple Search Ads. AdServices framework (attributionAttributionToken, iOS 14.3+) gives download attribution for ASA campaigns only. Singular ships AdServices as an optional (weak-linked) framework — the modern pattern. https://github.com/singular-labs/Singular-iOS-SDK
SKAdNetwork is NOT deep linking — it's privacy-preserving postback attribution with no link data delivered to the app. None of the vendors use SKAN for DDL; OptoLink shouldn't conflate them.
1.2 Universal Links config (the open-with-app-installed channel)
- Requires
applinks:<domain>entitlement + AASA (apple-app-site-association) hosted athttps://<link-domain>/.well-known/withappID = <TeamID>.<bundleID>. Same custom-domain-hosting model as Androidassetlinks.json— one CDN, two files. - Branch generates and hosts AASA for its
app.linkdomains and requires the app to register three host variants: the base domain,-alternate(needed for Universal Links + Deepviews for users without the app), and.testfor test keys. https://help.branch.io/developers-hub/docs/ios-basic-integration - Cold-start on iOS: SceneDelegate apps must fish
NSUserActivity/URLContextsout ofconnectionOptionsinscene(_:willConnectTo:)— every vendor documents this exact workaround (Branch ships aBranchScenewrapper class for it). https://help.branch.io/developers-hub/docs/ios-basic-integration
1.3 ATT handling patterns
- AppsFlyer:
waitForATTUserAuthorization(timeoutInterval:)— SDK holds its start until ATT resolves (or timeout); recommendrequestTrackingAuthorizationindidBecomeActive, thenstart(). https://dev.appsflyer.com/hc/docs/integrate-ios-sdk - Tenjin:
connect()inside the ATT completion handler so IDFA is present on grant; requiresNSUserTrackingUsageDescription. https://raw.githubusercontent.com/tenjin/sdk-llm-guides/main/guides/ios/llm-guide.md - Singular: "respects ATT automatically" — works without the prompt, just can't read IDFA on 14.5+. https://support.singular.net/hc/en-us/articles/12054824479387-iOS-SDK-Basic-Integration
- Lightweight entrants (LinkForty, WarpLink): no ATT at all — design DDL to need only IDFV + server-side signals, so a denied prompt costs nothing. Lesson for OptoLink: ATT should be optional plumbing, not a required integration step.
2. Vendor-by-vendor
2.1 Branch.io — iOS SDK
| Aspect | Fact |
|---|---|
| Integration shape | (1) Dashboard: enable Universal Links, set Team ID as "Apple App Prefix" + bundle IDs; (2) Xcode: applinks: entitlement for link domain + -alternate + .test variants; (3) initSession in didFinishLaunchingWithOptions + forward application(_:open:) and continue(userActivity:) to the SDK; (4) optional checkPasteboardOnInstall() for NativeLink DDL. https://help.branch.io/developers-hub/docs/ios-basic-integration , https://help.branch.io/developer-hub/docs/ios-advanced-features |
| Public API (Swift) | See snippet below. Callback shape: (params: [AnyHashable: Any]?, error: Error?) — one raw params dictionary carries link data + attribution flags (+is_first_session, +clicked_branch_link, +match_guaranteed). https://help.branch.io/developer-hub/docs/ios-full-reference |
| DDL on iOS | NativeLink™ pasteboard (100% match; must be enabled in dashboard; iOS 16 shows the paste prompt on first open) + probabilistic snapshot-matching fallback. DDL arrives through the same initSession callback as opens — no separate API. https://help.branch.io/docs/nativelink-deferred-deep-linking |
| SDK size | Not published (open source, Swift/ObjC). |
| Min iOS | iOS 12 (SPM changelog: "iOS 12 is now the min version", Xcode 15 min). Docs page still says iOS 9+ — stale. https://github.com/BranchMetrics/ios-branch-sdk-spm/blob/main/ChangeLog.md |
| Distribution | SPM: https://github.com/BranchMetrics/ios-branch-sdk-spm (source; separate repo from the CocoaPods repo). CocoaPods: pod 'BranchSDK'. Carthage: yes. |
| Privacy manifest | Yes — PrivacyInfo.xcprivacy shipped. Known gotcha: CocoaPods without use_frameworks! can't merge multiple privacy manifests → documented build failure + manual-merge workaround. https://help.branch.io/developers-hub/docs/ios-basic-integration |
import BranchSDK
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Branch.getInstance().initSession(launchOptions: launchOptions) { params, error in
// fires on install (deferred) AND every cold/warm open; params is one raw dictionary
if let click = params?["+clicked_branch_link"] as? Bool, click {
route(params ?? [:])
}
}
return true
}
// forward both entry points:
Branch.getInstance().application(app, open: url, options: options) // custom scheme
Branch.getInstance().continue(userActivity) // Universal Link
Notable extras: setRequestMetadataKey() must be called before init for partner metadata; deferInitForPlugin exists for RN/Unity but Branch explicitly warns not to use it for general deferral (verified in Branch.h). https://github.com/BranchMetrics/ios-branch-deep-linking-attribution/blob/master/Sources/BranchSDK/Public/Branch.h
2.2 AppsFlyer — iOS SDK V6/V7
| Aspect | Fact |
|---|---|
| Integration shape | Two-phase init: dev key + Apple App ID in didFinishLaunching, start() deferred to applicationDidBecomeActive (or ATT completion). Forward continue(userActivity:) and handleOpen(url). https://dev.appsflyer.com/hc/docs/integrate-ios-sdk |
| Public API (Swift) | Legacy conversion data: AppsFlyerConversionListener → onConversionDataSuccess([AnyHashable: Any]) with is_first_launch; modern: Unified Deep Linking (UDL) — deepLinkDelegate + didResolveDeepLink(DeepLinkResult); result has status (.found/.notFound/.failure) + typed DeepLink object with isDeferred, deeplinkValue, clickEvent. https://dev.appsflyer.com/hc/docs/dl_ios_unified_deep_linking , https://dev.appsflyer.com/hc/docs/conversion-data-ios |
| DDL on iOS | UDL fires on first open, matches install→click server-side; privacy masking: new users only get deep_link_value + deep_link_sub1-10 — media_source, af_sub1-5 etc. return null (existing users get everything). Legacy OneLinks route new users through onConversionDataSuccess ("eDDL", also extended for DMA consent). https://dev.appsflyer.com/hc/docs/dl_ios_unified_deep_linking |
| ATT | waitForATTUserAuthorization(timeoutInterval: 60) + manual ATTrackingManager.requestTrackingAuthorization in didBecomeActive. https://dev.appsflyer.com/hc/docs/integrate-ios-sdk |
| SDK size | Not published; distributed as a closed static XCFramework. |
| Min iOS | Docs say iOS 9+ (tvOS 9+, macOS 10.13) — the stalest minimum claim of the group. https://dev.appsflyer.com/hc/docs/ios-sdk |
| Distribution | SPM: https://github.com/AppsFlyerSDK/AppsFlyerFramework — binaryTarget pointing at a zip'd static xcframework with a checksum (closed source, verified in Package.swift). CocoaPods: yes. |
| Privacy manifest | Yes — "Implement Privacy Manifest in your app" support doc; SDK checks ATT status before requests; tracking domains must be recorded in the manifest. https://support.appsflyer.com/hc/en-us/articles/21677433322641-Implement-Privacy-Manifest-in-your-app |
// didFinishLaunchingWithOptions:
AppsFlyerLib.shared().appsFlyerDevKey = "<DEV_KEY>"
AppsFlyerLib.shared().appleAppID = "<APPLE_APP_ID>"
AppsFlyerLib.shared().deepLinkDelegate = self
// applicationDidBecomeActive (after ATT):
AppsFlyerLib.shared().start()
extension AppDelegate: DeepLinkDelegate {
func didResolveDeepLink(_ result: DeepLinkResult) {
switch result.status {
case .found:
if let dl = result.deepLink, dl.isDeferred == true { // install was attributed
route(dl.deeplinkValue, dl.clickEvent)
}
case .notFound: break // organic install
case .failure: print(result.error!)
@unknown default: break
}
}
}
2.3 Adjust — iOS SDK v5
| Aspect | Fact |
|---|---|
| Integration shape | ADJConfig(appToken:environment:) (sandbox/production), set delegate, Adjust.initSdk(config) in didFinishLaunching. Direct deep links: call Adjust.processDeeplink(AdjustDeeplink(url:)) from your open/continue handlers. https://dev.adjust.com/en/sdk/ios/features/deep-links/deferred/ |
| Public API (Swift) | The best-designed DDL hook of the big three: a delegate method that returns a Bool deciding whether the SDK opens the resolved link (true = SDK re-dispatches it through application(_:open:options:) / scene(_:openURLContexts:) / .onOpenURL, so the app's existing routing handles it; false = app keeps full control, e.g. wait for onboarding). Attribution is a separate typed AdjustAttribution object. https://dev.adjust.com/en/sdk/ios/features/deep-links/deferred/ |
| DDL on iOS | Click → store → install → first /session (+/attribution) network call → server matches click and returns the deferred link in the session response (optimized DDL; legacy used the attribution response). Server converts brandname.go.link URLs to the app's configured custom scheme (example://summer-clothes?promo=beach&adj_t=…) before delivery. https://dev.adjust.com/en/sdk/ios/features/deep-links/deferred/ |
| SDK size | Not published; source distribution, core self-contained ObjC. |
| Min iOS | iOS 12 (v5 hard requirement — apps targeting lower must stay on v4). https://dev.adjust.com/en/sdk/migration/ios/v4-to-v5 |
| Distribution | SPM: github.com/adjust/ios_sdk — source target AdjustSdk (+ AdjustUnsigned variant without the signature lib), verified in Package.swift (platforms: [.iOS(.v12)]). CocoaPods Adjust pod, Carthage too. |
| Privacy manifest | Yes — changelog: "Added Privacy Manifest for the Adjust SDK"; also routes consented traffic to consent.adjust.com with payload restrictions. https://github.com/adjust/ios_sdk/blob/master/CHANGELOG.md |
class AppDelegate: UIResponder, UIApplicationDelegate, AdjustDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: …) -> Bool {
let config = ADJConfig(appToken: "{token}", environment: ADJEnvironmentSandbox)
config?.delegate = self
Adjust.initSdk(config)
return true
}
// DDL: return false → you route manually; true → SDK opens it via the system
func adjustDeferredDeeplinkReceived(_ deeplink: URL?) -> Bool {
guard let deeplink else { return false }
storeForAfterOnboarding(deeplink) // e.g. stash until login done
return false
}
}
2.4 Singular — brief
- Init:
let config = SingularConfig(apiKey:andSecret:)→Singular.start(config); or the one-shotstartSession(_:withKey:andLaunchOptions:withSingularLinkHandler:)(verified inheaders/Singular.h). https://github.com/singular-labs/Singular-iOS-SDK - DDL: the singular (sic) link handler covers direct + deferred in one callback:
SingularLinkParamscarriesdeeplink,passthrough,isDeferred; short-link resolution has a configurable timeout (andShortLinkResolveTimeout, default ~10s on Android / documented in iOS deep-link docs). https://support.singular.net/hc/en-us/articles/36197770789915-iOS-SDK-Supporting-Deep-Links - Min iOS: 12 (verified in
Package.swift:.iOS(.v12)). Distribution: SPM (binaryTargetxcframework, closed) + CocoaPods. AdServices framework must be added optional/weak (iOS 14.3+ only) — ASA attribution. https://github.com/singular-labs/Singular-iOS-SDK - ATT: automatic; needs
NSUserTrackingUsageDescriptionfor IDFA. Privacy manifest: shipped with the framework. https://support.singular.net/hc/en-us/articles/12054824479387-iOS-SDK-Basic-Integration
2.5 Kochava — brief
- Init: Swift-native modular SDK —
KochavaMeasurementSPM package (depends onKochavaNetworking/KochavaCore); v8 is SPM-exclusive (CocoaPods dropped: "remove any Kochava Cocoapods … add the Swift Packages"). Current release floor: iOS 15.5 (highest of the group), XCFramework binary. https://github.com/Kochava/Apple-SwiftPackage-KochavaMeasurement-XCFramework , https://support.kochava.com/articles/apple-sdk/ - DDL:
Deeplink.process— pass a URI for opens, null/empty to ask for a deferred deeplink;eventAutoSendflag controls auto-logging a_Deeplinkevent. Attribution response carries the deferred deeplink +is_first_install. https://support.kochava.com/articles/sdk-integration/20902-ios-sdk-integration/ - Documented gotcha worth designing against: on reinstall, Kochava re-delivers the original install's deferred deeplink — apps must check
is_first_installor they'll re-run a 2-week-old promo deep link. https://support.kochava.com/articles/reference-information/15692-attribution-response-examples/ - Privacy manifest guidance published; company context: FTC sensitive-location settlement (see android research §5.1 — applies to the same company).
2.6 Tenjin — brief (the lean end of MMPs)
- Init:
TenjinSDK.getInstance("<SDK_KEY>")every launch, thenTenjinSDK.connect()— inside the ATT completion handler when ATT is used; accounts can be suspended ifconnectisn't called on every launch. https://raw.githubusercontent.com/tenjin/sdk-llm-guides/main/guides/ios/llm-guide.md - DDL:
TenjinSDK.connectWithDeferredDeeplink(url)orregisterDeepLinkHandler \{ params, error in }+connect(). - Min iOS: 14. Distribution: SPM (
tenjin/tenjin-ios-spm) + manual xcframework. Thin wrapper over a binary framework.
2.7 Firebase Dynamic Links — DEPRECATED (the market gap)
- Shut down Aug 25, 2025. All FDL links (page.link + custom domains) now 404; Short Links & Link Stats APIs dead. https://firebase.google.com/support/dynamic-links-faq
- Google's official recommendation: two-track — (a) installed-app deep linking → plain Universal Links (official migration guide, optionally AASA hosting via Firebase Hosting); (b) full feature parity incl. deferred → third-party vendors, named list Adjust, Airbridge, AppsFlyer, Bitly, Branch, Kochava, Singular ("not vetted by Google"). https://firebase.google.com/support/guides/app-links-universal-links , https://firebase.google.com/support/dynamic-links-faq
- The gap: Apple still ships no first-party deferred deep linking on iOS, and Google's exit removed the only free first-party one. Migration guides describe the hole bluntly: DDL is the one FDL feature platform links can't replace ("handle deferred deep linking, or accept the loss"). https://link.boo/guides/firebase-dynamic-links-replacement
- Consequence: a wave of FDL-replacement products (see §2.8) and vendor capture (AppsFlyer/Branch both published FDL-migration pitches). OptoLink's native SDKs are aimed exactly at this vacated "lightweight, self-hostable DDL" slot.
2.8 Modern lightweight / open-source entrants
| Project | What it is | iOS API shape | Why it matters for OptoLink |
|---|---|---|---|
| LinkForty (MIT, self-hosted) | Open-source Branch/FDL alternative: core server + SDKs; "privacy-first, no per-click pricing". iOS: 100% Swift, async/await API, zero dependencies, iOS 16+, SPM/CocoaPods/Carthage. DDL via "privacy-compliant fingerprinting", no IDFA. https://github.com/LinkForty/mobile-sdk-ios | try await LinkForty.shared.initialize(config:); handleDeepLink(url:); onDeepLink \{ url, data in }; onDeferredDeepLink \{ data in } (nil = organic); trackEvent/trackRevenue async; SwiftUI .linkfortyScreen() modifier | Closest existing template of the minimal modern shape — closures for link delivery, async for network ops, no ATT, config-object init, attributionWindowHours configurable |
| WarpLink (commercial, tiny) | DDL-focused lightweight vendor. Two-call API: configure(apiKey:) + checkDeferredDeepLink exactly once on first launch. Match cascade: IDFV (conf 1.0) → timezone-enriched fingerprint tiers (0.85→0.20 with explicit confidence scores + matchGuaranteed), server computes hashes so iCloud Private Relay only degrades confidence, doesn't break matching; no ATT needed. https://warplink.app/blog/deferred-deep-linking-ios | Result object: matchType, matchConfidence, matchGuaranteed — guidance: gate sensitive actions on guaranteed, route public content above ~0.5 | The exposed confidence/matchType in the public result is the honest version of Branch's opaque +match_guaranteed — worth copying |
| Flinku / LinkMe / DeepOne.io | Small FDL-replacement SaaS entrants, all advertising "deferred deep linking, Universal Links, attribution" on GitHub topic deferred-deep-linking. https://github.com/topics/deferred-deep-linking , https://flinku.dev/ , https://r-dev-limited.github.io/li-nk.me-ios-sdk/ | Standard shape (init + handle + deferred callback) | Proof the post-FDL slot is contested; differentiation is price/self-hosting, not API novelty |
3. Cross-vendor comparison (iOS)
| Branch | AppsFlyer | Adjust | Singular | Kochava | Tenjin | LinkForty | |
|---|---|---|---|---|---|---|---|
| Min iOS | 12 (changelog; docs say 9, stale) | 9+ (docs, stale) | 12 (v5 hard floor) | 12 (Package.swift) | 15.5 (current) | 14 | 16 |
| SPM | ✅ source (separate repo) | ✅ but closed binary xcframework | ✅ source (+AdjustUnsigned) | ✅ closed binary xcframework | ✅ SPM-exclusive since v8 (binary) | ✅ | ✅ source |
| CocoaPods | ✅ BranchSDK | ✅ | ✅ Adjust | ✅ | ❌ dropped in v8 | ✅ | ✅ |
| DDL mechanism | Pasteboard NativeLink + fingerprint | Server match via UDL/conversion (+ pasteboard variant) | Server match in session response | Server match in link handler (10s timeout) | Attribution response + null-URI query | connectWithDeferredDeeplink | Fingerprint, no IDFA |
| DDL API style | Same initSession callback as opens | Separate delegate (didResolveDeepLink), status enum + isDeferred | Delegate returns Bool (open-or-not) | One handler, isDeferred flag in params | Explicit Deeplink.process(nil) | Explicit deferred call + handler | Dedicated onDeferredDeepLink closure (nil = organic) |
| Privacy manifest | ✅ (CocoaPods merge gotcha) | ✅ (+ tracking-domain rules) | ✅ (changelog-verified) | ✅ | ✅ (guidance) | (thin wrapper; not surfaced) | n/a (self-hosted, MIT) |
| Source visible? | ✅ MIT | ❌ binary | ✅ MIT | ❌ binary | ❌ binary | ❌ binary | ✅ MIT |
iOS deltas vs the Android research: (1) no deterministic OS referrer → every vendor leans on pasteboard/fingerprint, so DDL reliability is worse on iOS and matchConfidence-style honesty matters more; (2) ATT shapes init order (AppsFlyer/Tenjin) or is designed away entirely (lightweight vendors); (3) privacy manifests are the iOS analog of Android's Data-safety pain; (4) min-floor drift: Android converged on 21, iOS is diverging (12 → 14 → 15.5) because Swift concurrency and visionOS-era packaging push floors up — pick a floor deliberately, don't inherit one.
4. Criticisms & design signals specific to iOS
- The pasteboard prompt tax. Branch documents that NativeLink on iOS 16+ shows the native paste prompt during first open; their docs now carry UX warnings. Any DDL design that silently reads UIPasteboard is one OS update from breaking. Lesson: make pasteboard matching opt-in and expect the prompt. https://help.branch.io/docs/nativelink-deferred-deep-linking
- Closed-binary SPM distribution is a trust regression. AppsFlyer and Singular "support SPM" only by shipping opaque static xcframeworks — unauditable (mirrors the Android hash-asset-blob complaint). Adjust ships readable source via SPM. Lesson: OptoLink SDK = source package, inspectable. (Verified in both
Package.swiftfiles.) - Stale minimums and doc drift. Branch docs say iOS 9+, reality is iOS 12; AppsFlyer still claims iOS 9+ on its overview. Lesson: publish one machine-readable source of truth (Package.swift platforms) and keep marketing docs generated from it.
- AppDelegate/SceneDelegate split is the #1 integration bug farm. Every vendor docs the scene cold-start workaround separately; Branch ships a whole
BranchScenecompatibility class. Lesson: a singlehandleLaunch/handleDeepLinkentry point that internally handles both worlds removes the app's biggest source of "link works cold but not warm" tickets. - Kochava's reinstall replay. Attribution response re-delivers old deferred deeplinks on reinstall; apps must gate on
is_first_install. Lesson: OptoLink should make install-vs-open explicit in the payload, and not replay expired promotions. https://support.kochava.com/articles/reference-information/15692-attribution-response-examples/ - AppsFlyer's new-user masking is well-intentioned but confusing — UDL silently nulls
media_source/af_subfor new installs, generating "why is this key missing" tickets. Lesson: return a documented, complete payload with an explicit field (matchType/isNewUser) instead of per-key nulls. https://dev.appsflyer.com/hc/docs/dl_ios_unified_deep_linking - ATT-wait timeouts are footguns.
waitForATTUserAuthorization(60)can hold attribution up to a minute behind a prompt the user may never answer. Lesson: OptoLink's pipeline must work with zero ATT coupling. - FDL's 404 apocalypse applies double on iOS. No platform fallback existed, so every FDL link on iOS is simply dead. Lesson (same as Android §5.9): links must live on the customer's own domain with AASA hosted from it.
5. Synthesis: what a minimal modern iOS deep-link SDK looks like in 2026
API shape (recommendation)
// 1. Init — config object, async or callback-free; once, in AppDelegate.didFinishLaunching
try OptoLink.configure(OptoLinkConfig(
apiKey: "…",
baseUrl: URL(string: "https://go.customer.com")!)) // customer's own domain, AASA hosted from it
// 2. Direct deep links — ONE entry point for every iOS surface
OptoLink.handleDeepLink(url) // scene(_:openURLContexts:), .onOpenURL
OptoLink.handleUserActivity(activity) // scene(_:continue:), cold-start connectionOptions
// internally emits: .direct(OptoLinkData) to the handler below
// 3. Delivery — a single async stream + closure sugar for UIKit apps
OptoLink.onLink { (link: OptoLinkResult) in … } // or: for await link in OptoLink.links
// OptoLinkResult: .direct(data) | .deferred(data, matchType, confidence) | .organic
// 4. Deferred — explicit, once, on first launch; ALSO fires through (3)
await OptoLink.resolveDeferredLink() // server match; returns .organic when nothing matches
Concretely:
- Callbacks for delivery, async/await for actions. Deep-link delivery must work in
didFinishLaunchingbefore any UI exists → a registration-style handler (Branch/Adjust/Kochava model). Create-link, resolve, events →asyncthrows (LinkForty/WarpLink generation). OfferAsyncStreamas the modern delivery option, not the only one. - Typed result, status +
isDeferred+matchType/confidence (AppsFlyer'sDeepLinkResultenum + WarpLink's confidence, merged). No raw[String: Any](Branch) and no silent per-key nulling (AppsFlyer). - Adjust-style
shouldOpenhook for deferred links: returnfalse, app routes after onboarding. This is the single best API decision in the market. - One launch handler covering AppDelegate + SceneDelegate + SwiftUI (kills the
BranchSceneclass of problems). isFirstLaunch/isReinstallexplicit in the payload (Kochava's replay gotcha, done right).
Include
- Universal Links + custom scheme ingestion; AASA-hosting guidance for the customer domain (server-side).
- Deferred DDL via server-side match on first session call (IDFV + click context, opt-in fingerprint tier with confidence), delivered through the same result type as opens.
- Pasteboard matching as opt-in, documented with the iOS 16 prompt caveat.
PrivacyInfo.xcprivacyshipped in the package; zero required ATT coupling; optional AdServices (weak-linked) for Apple Search Ads.- SPM source distribution, min iOS 12–13 (adjustable constant), zero third-party dependencies, SwiftUI convenience modifier.
Skip
- IDFA/ATT plumbing, SKAdNetwork (not deep linking), geolocation/monetized data (FTC v. Kochava), any binary xcframework distribution, auto-swizzling of AppDelegate (explicit calls only — the one place to not copy Branch), auto-read of pasteboard by default, and any vendor-hosted link domain (that's the FDL failure mode).
Filed by the OptoLink iOS SDK research pass. Cross-references: android-sdk-competitive-research.md (platform patterns), ios-sdk-android-parity-notes.md, ios-sdk-contracts-notes.md.