Quick answer: Read launchOptions[.url] in didFinishLaunchingWithOptions, cache it, and deliver to your engine after init finishes. iOS uses a different callback for cold-start vs hot-start deep links.

A marketing email links to mygame://event/halloween. Tapping the link with the game already running takes the player to the Halloween event — correct. Tapping the same link when the game is not running launches the game but lands on the title screen, ignoring the parameter.

Two Entry Points, One URL

iOS routes deep links through two different AppDelegate methods:

Most tutorial code handles only the hot-start callback. Cold-start URLs disappear.

The Fix

// AppDelegate.swift (or .mm wrapper for Unity/Unreal)
class AppDelegate: UIResponder, UIApplicationDelegate {
    static var pendingURL: URL?

    func application(_ app: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        if let url = launchOptions?[.url] as? URL {
            AppDelegate.pendingURL = url
        }
        return true
    }

    func application(_ app: UIApplication,
        open url: URL,
        options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
        deliverURL(url)
        return true
    }

    static func flushPendingURL() {
        if let u = pendingURL {
            deliverURL(u)
            pendingURL = nil
        }
    }
}

The cold-start URL is cached. The engine calls flushPendingURL after it’s ready — for Unity, from the first Start() on a bootstrap GameObject; for Unreal, from a custom UFUNCTION invoked from Blueprint after BeginPlay.

Universal Links Have an Extra Callback

For Universal Links (https URLs registered to your domain):

func application(_ app: UIApplication,
    continue userActivity: NSUserActivity,
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
       let url = userActivity.webpageURL {
        deliverURL(url)
    }
    return true
}

And cache it similarly if launched cold:

if let activity = launchOptions?[.userActivityDictionary] as? [String: Any],
   let u = activity["UIApplicationLaunchOptionsUserActivityKey"] as? NSUserActivity,
   let url = u.webpageURL {
    AppDelegate.pendingURL = url
}

Engine Integration

For Unity, the bootstrap GameObject calls a native plugin method that invokes flushPendingURL; the plugin uses UnitySendMessage to call back into managed code with the URL string. For Unreal, expose a static FString and a Blueprint Function Library node to retrieve it.

Verifying

Force-quit the game. Tap a deep link from email or a test webpage. The game should launch and route to the deep-link destination, not the title screen. Test with 5+ different URLs to ensure parameter parsing handles edge cases.

“Hot-start and cold-start URLs come through different callbacks. Handle both, or half your deep links silently miss.”

Test cold-start by force-quitting from the multitask switcher — not just backgrounding. They’re different code paths.