Receive Links on iOS
Handle incoming deep links in your iOS app using the ULink SDK's reactive streams and URL handling APIs.
Prerequisites
Before receiving links, ensure you've completed the SDK setup:
SDK Integration
- ULinkSDK installed via Swift Package Manager or CocoaPods
ULink.initializecalled in yourAppDelegatewith valid configuration- URL scheme configured in Xcode (Targets → Info → URL Types)
- Associated domains configured if using universal links (Signing & Capabilities)
ULink Dashboard Configuration
- Project settings configured in the ULink dashboard (bundle ID, URL scheme, domain)
- API key generated from Dashboard → Settings → API Keys → Generate API Key
- Domain configured in your project (Domains → Add Domain)
If you haven't completed these steps, follow the iOS Getting Started guide to set up the SDK and configure your project.
Understanding Link Reception
The ULink SDK provides two main ways to receive links:
- Automatic Handling (Recommended): The SDK automatically processes incoming URLs when
enableDeepLinkIntegrationis enabled - Manual Handling: You manually forward URLs to the SDK for processing
Both approaches use Combine streams to emit resolved link data that you can observe and react to.
1. Automatic Handling (Recommended)
When enableDeepLinkIntegration is enabled in your ULinkConfig (default: true), the SDK automatically processes initial URLs. You need to forward incoming URLs from your AppDelegate to the SDK.
Step 1.1: Forward URLs in AppDelegate
Add URL handling methods to your AppDelegate:
import UIKit
import ULinkSDK
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var cancellables = Set<AnyCancellable>()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Initialize ULink SDK
let config = ULinkConfig(
apiKey: "your-api-key",
debug: true,
enableDeepLinkIntegration: true // Enable automatic handling
)
ULink.initialize(config: config)
// Set up stream listeners
setupLinkListeners()
return true
}
// Handle custom URL schemes (e.g., myapp://)
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
return ULink.shared.handleIncomingURL(url)
}
// Handle universal links (https://)
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return false
}
return ULink.shared.handleIncomingURL(url)
}
}
application(_:open:options:): Handles custom URL schemes (e.g.,myapp://path)application(_:continue:restorationHandler:): Handles universal links (e.g.,https://yourdomain.com/path)- Both methods should return
trueif the URL was handled,falseotherwise
If you're using SwiftUI, you can handle URLs in your App struct:
import SwiftUI
import ULinkSDK
@main
struct MyApp: App {
init() {
let config = ULinkConfig(apiKey: "your-api-key", enableDeepLinkIntegration: true)
ULink.initialize(config: config)
}
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
ULink.shared.handleIncomingURL(url)
}
}
}
}
Step 1.2: Observe Link Streams
Use Combine to observe resolved link data:
import Combine
import ULinkSDK
func setupLinkListeners() {
// Listen for dynamic links
ULink.shared.dynamicLinkStream
.sink { [weak self] resolved in
self?.handleDynamicLink(resolved)
}
.store(in: &cancellables)
// Listen for unified links
ULink.shared.unifiedLinkStream
.sink { [weak self] resolved in
self?.handleUnifiedLink(resolved)
}
.store(in: &cancellables)
}
func handleDynamicLink(_ data: ULinkResolvedData) {
print("Received dynamic link:")
print(" Slug: \(data.slug ?? "N/A")")
print(" Parameters: \(data.parameters ?? [:])")
// Navigate based on parameters
if let screen = data.parameters?["screen"] as? String {
navigateToScreen(screen, parameters: data.parameters)
}
}
func handleUnifiedLink(_ data: ULinkResolvedData) {
print("Received unified link:")
print(" Slug: \(data.slug ?? "N/A")")
// Unified links typically don't have parameters
// Handle marketing/redirect links here
}
dynamicLinkStream: Emits dynamic links with parameters for in-app navigationunifiedLinkStream: Emits unified links for marketing/redirect scenarios- Both streams emit
ULinkResolvedDataobjects containing the resolved link information
2. Manual Handling
If you prefer manual control or need to process URLs before SDK handling:
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard let url = userActivity.webpageURL else { return false }
// Process URL manually if needed
// ... your custom logic ...
// Forward to SDK
ULink.shared.handleDeepLink(url: url)
return true
}
- You need to validate or transform URLs before processing
- You want to handle certain URLs differently
- You're integrating with other deep linking frameworks
3. Access Initial and Last Links
The SDK provides methods to access links that arrived before your app was ready or after a cold start.
Get Initial Deep Link
When your app launches from a link, you can retrieve the initial link data:
Task {
if let initialLink = await ULink.shared.getInitialDeepLink() {
print("App launched from link: \(initialLink.slug ?? "N/A")")
// Navigate to the appropriate screen
handleDynamicLink(initialLink)
}
}
Get Last Link Data
Retrieve the last resolved link data (useful for resuming state after app restarts):
if let lastLink = ULink.shared.getLastLinkData() {
print("Last link: \(lastLink.slug ?? "N/A")")
// Use lastLink to restore app state
handleDynamicLink(lastLink)
}
getLastLinkData() returns data only when persistLastLinkData is enabled in your ULinkConfig (it's on by default). If you disabled it, re-enable the setting before relying on this API.
4. Deferred Deep Linking
Deferred deep linking allows you to deep link users to specific content even if they haven't installed your app yet. When they install and open the app for the first time, the original deep link is preserved and delivered.
How It Works
- User clicks a ULink but doesn't have the app installed
- User is redirected to the App Store and installs the app
- User opens the app for the first time
- SDK retrieves the original link data and delivers it via the link streams
Automatic Deferred Link Handling
By default, the SDK automatically checks for deferred links on first launch:
ULink.initialize(config: ULinkConfig(
apiKey: "your-api-key",
autoCheckDeferredLink: true // Default: true
))
// Deferred links are delivered via the same streams as regular links
ULink.shared.dynamicLinkStream
.sink { [weak self] data in
// Check if this is a deferred link
if data.isDeferred {
print("Deferred link received!")
print("Match type: \(data.matchType ?? "unknown")")
// matchType is "fingerprint" on iOS
}
self?.handleLink(data)
}
.store(in: &cancellables)
Link Data Properties for Deferred Links
| Property | Type | Description |
|---|---|---|
isDeferred | Bool | true if this link came from deferred deep linking |
matchType | String? | Match method (typically "fingerprint" on iOS) |
Manual Deferred Link Check
For more control, disable automatic checking and call checkDeferredLink() manually:
ULink.initialize(config: ULinkConfig(
apiKey: "your-api-key",
autoCheckDeferredLink: false // Disable automatic check
))
// Later, after user completes onboarding or grants permissions:
await ULink.shared.checkDeferredLink()
On iOS, when your app makes its first network request, the system shows a permission dialog asking the user to allow network access. The problem is that the network request is already in flight when this dialog appears — the request will fail regardless of whether the user accepts or denies.
This means if you initialize the SDK immediately on app launch with autoCheckDeferredLink: true, the deferred link check will fail before the user can even respond to the dialog.
Delayed Initialization (Recommended)
Delay SDK initialization entirely until after your onboarding flow or until after the user has made their first successful network request:
import UIKit
import ULinkSDK
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// ❌ DON'T initialize ULink here immediately
// ULink.initialize(config: ...)
return true
}
}
// In your onboarding view controller or after first successful network call:
class OnboardingViewController: UIViewController {
func completeOnboarding() {
// ✅ Initialize ULink only after onboarding
ULink.initialize(config: ULinkConfig(
apiKey: "your-api-key",
autoCheckDeferredLink: true // Now safe to auto-check
))
// Navigate to home screen
navigateToHome()
}
}
- Don't initialize ULink immediately in
application(_:didFinishLaunchingWithOptions:) - Complete your app's onboarding or make another network request first (e.g., login, fetch config)
- Then call
ULink.initialize()— by this point, iOS has already granted network permission - The deferred link check will now succeed and deliver via your link streams
How Matching Works (iOS)
On iOS, the SDK uses fingerprint matching to match deferred links:
- Uses device characteristics (screen resolution, timezone, language)
- 70-90% accuracy
- This is the only matching method available on iOS since Apple doesn't provide an equivalent to Android's Install Referrer API
Limitations
- One-Time Check: Deferred link check only runs once per app installation
- 24-Hour Expiry: Deferred link data expires 24 hours after the original click
- Network Required: The check requires an active network connection
- Network Permission: Initialization must be delayed until after first successful network request
- Fingerprint-Only: iOS uses probabilistic matching (no deterministic option like Android)
5. Complete Example
Here's a complete example showing how to set up link reception in an iOS app:
import UIKit
import Combine
import ULinkSDK
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var cancellables = Set<AnyCancellable>()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Initialize SDK
let config = ULinkConfig(
apiKey: "your-api-key",
debug: true,
enableDeepLinkIntegration: true,
persistLastLinkData: true
)
ULink.initialize(config: config)
// Set up link listeners
setupLinkListeners()
// Check for initial link
Task {
if let initialLink = await ULink.shared.getInitialDeepLink() {
await MainActor.run {
handleLink(initialLink)
}
}
}
return true
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
return ULink.shared.handleIncomingURL(url)
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return false
}
return ULink.shared.handleIncomingURL(url)
}
private func setupLinkListeners() {
ULink.shared.dynamicLinkStream
.sink { [weak self] data in
self?.handleLink(data)
}
.store(in: &cancellables)
ULink.shared.unifiedLinkStream
.sink { [weak self] data in
self?.handleUnifiedLink(data)
}
.store(in: &cancellables)
}
private func handleLink(_ data: ULinkResolvedData) {
guard let window = window,
let rootVC = window.rootViewController else { return }
// Navigate based on link data
if let screen = data.parameters?["screen"] as? String {
switch screen {
case "product":
if let productId = data.parameters?["productId"] as? String {
navigateToProduct(productId, from: rootVC)
}
case "profile":
if let userId = data.parameters?["userId"] as? String {
navigateToProfile(userId, from: rootVC)
}
default:
break
}
}
}
private func handleUnifiedLink(_ data: ULinkResolvedData) {
// Handle marketing/redirect links
print("Unified link received: \(data.slug ?? "N/A")")
}
private func navigateToProduct(_ productId: String, from viewController: UIViewController) {
// Your navigation logic
}
private func navigateToProfile(_ userId: String, from viewController: UIViewController) {
// Your navigation logic
}
}
5. Verify Universal Link Configuration
If your app never opens when tapping links, verify the configuration:
Check the AASA File
The Apple App Site Association (AASA) file must be accessible and valid:
- Visit
https://<your-domain>/.well-known/apple-app-site-associationin a browser - Or run:
curl https://links.shared.ly/.well-known/apple-app-site-association - It should return JSON (no redirects, no HTML)
Test on Simulator
xcrun simctl openurl booted "https://links.shared.ly/sample-slug"
Then open Console.app, filter by your bundle ID, and confirm continueUserActivity is triggered.
Confirm Device Trust
On a physical device:
- Delete and reinstall the app
- iOS re-fetches the AASA file during install
- If the file is invalid, universal links will fall back to Safari
If any step fails, fix the AASA file or domain configuration before retesting.
Best Practices
Link Handling
- Always check for initial links: Use
getInitialDeepLink()when your app launches to handle links that opened the app - Use weak references: When subscribing to streams, use
[weak self]to avoid retain cycles - Handle both stream types: Set up listeners for both
dynamicLinkStreamandunifiedLinkStream - Validate parameters: Always validate and safely unwrap parameters before using them
Navigation
- Centralize navigation logic: Create a single method to handle all link-based navigation
- Handle edge cases: Account for missing parameters or invalid link data
- Provide fallbacks: If navigation fails, show an appropriate error or fallback screen
Testing
- Test both URL types: Test both custom schemes (
myapp://) and universal links (https://) - Test cold starts: Ensure links work when the app is launched from a closed state
- Test warm starts: Ensure links work when the app is already running
- Check logs: Enable debug mode and monitor logs for link processing
Troubleshooting
Links Not Opening App
- Verify URL scheme is configured in Xcode
- Check Associated Domains are properly set up
- Verify AASA file is accessible and valid
- Ensure domain matches dashboard configuration
Streams Not Emitting
- Confirm SDK initialization completed successfully
- Check that
enableDeepLinkIntegrationis enabled - Verify URLs are being forwarded to
handleIncomingURL - Enable debug mode to see processing logs
Parameters Missing
- Verify link was created with parameters
- Check that you're listening to the correct stream (dynamic vs unified)
- Ensure parameters are accessed safely with optional unwrapping
Next Steps
- Create Links → iOS - Learn how to create links programmatically
- Troubleshoot & Test Deep Links - Debug and verify your links