Skip to main content

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.initialize called in your AppDelegate with valid configuration
  • URL scheme configured in Xcode (Targets → Info → URL Types)
  • Associated domains configured if using universal links (Signing & Capabilities)
  • 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.

The ULink SDK provides two main ways to receive links:

  1. Automatic Handling (Recommended): The SDK automatically processes incoming URLs when enableDeepLinkIntegration is enabled
  2. 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.

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)
}
}
URL Handling Methods
  • 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 true if the URL was handled, false otherwise
SwiftUI Apps

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)
}
}
}
}

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
}
Stream Types
  • dynamicLinkStream: Emits dynamic links with parameters for in-app navigation
  • unifiedLinkStream: Emits unified links for marketing/redirect scenarios
  • Both streams emit ULinkResolvedData objects 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
}
When to Use Manual Handling
  • You need to validate or transform URLs before processing
  • You want to handle certain URLs differently
  • You're integrating with other deep linking frameworks

The SDK provides methods to access links that arrived before your app was ready or after a cold start.

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)
}
}

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)
}
Persistence Required

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

  1. User clicks a ULink but doesn't have the app installed
  2. User is redirected to the App Store and installs the app
  3. User opens the app for the first time
  4. SDK retrieves the original link data and delivers it via the link streams

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)
PropertyTypeDescription
isDeferredBooltrue if this link came from deferred deep linking
matchTypeString?Match method (typically "fingerprint" on iOS)

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()
iOS Network Permission — Critical Issue

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.

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()
}
}
Best Practice for iOS
  1. Don't initialize ULink immediately in application(_:didFinishLaunchingWithOptions:)
  2. Complete your app's onboarding or make another network request first (e.g., login, fetch config)
  3. Then call ULink.initialize() — by this point, iOS has already granted network permission
  4. 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

  1. One-Time Check: Deferred link check only runs once per app installation
  2. 24-Hour Expiry: Deferred link data expires 24 hours after the original click
  3. Network Required: The check requires an active network connection
  4. Network Permission: Initialization must be delayed until after first successful network request
  5. 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
}
}

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:

  1. Visit https://<your-domain>/.well-known/apple-app-site-association in a browser
  2. Or run:
    curl https://links.shared.ly/.well-known/apple-app-site-association
  3. 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:

  1. Delete and reinstall the app
  2. iOS re-fetches the AASA file during install
  3. 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

  • 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 dynamicLinkStream and unifiedLinkStream
  • Validate parameters: Always validate and safely unwrap parameters before using them
  • 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

  • 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 enableDeepLinkIntegration is 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