iOS
Open your app from its links with universal links, resolve each link with the API, and find the link that led to an install.
This guide uses Swift. Replace demo with your app's name and YOUR_API_KEY with its key: the sandbox key in debug builds and the live key in release builds.
1. Add the associated domain
In Xcode, select your app's target, open Signing & Capabilities, add Associated Domains and enter:
applinks:demo.dev.thedbl.inkAdd one entry per custom domain too, such as applinks:link.example.com. The domains must match the team ID and bundle ID in your app's settings, because iOS checks them against the apple-app-site-association file Deep Blue Links serves.
2. Resolve the link that opened the app
Send the URL to POST /api/links/resolve. A 404 means the URL isn't one of your short links, such as a link's full URL, so route by the URL itself.
import Foundation
struct ResolvedLink: Decodable {
let id: Int
let longUrl: String
let shortUrl: String
let isSandbox: Bool?
}
struct DeepBlueLinksError: LocalizedError {
let status: Int
var errorDescription: String? { "Deep Blue Links answered \(status)" }
}
enum DeepBlueLinks {
static let apiKey = "YOUR_API_KEY"
static func resolve(_ url: URL) async throws -> ResolvedLink? {
try await post("https://dev.thedbl.ink/api/links/resolve", body: ["url": url.absoluteString])
}
static func post<Body: Encodable>(_ endpoint: String, body: Body) async throws -> ResolvedLink? {
var request = URLRequest(url: URL(string: endpoint)!)
request.httpMethod = "POST"
request.setValue(apiKey, forHTTPHeaderField: "Api-Key")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(body)
let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
if status == 404 { return nil }
guard status == 200 else { throw DeepBlueLinksError(status: status) }
return try JSONDecoder().decode(ResolvedLink.self, from: data)
}
}In a SwiftUI app, universal links arrive in onOpenURL:
@main
struct DemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
Task {
let link = try? await DeepBlueLinks.resolve(url)
let destination = link.flatMap { URL(string: $0.longUrl) } ?? url
Router.shared.open(path: destination.path, query: destination.query)
}
}
}
}
}Router stands for your app's own navigation. When link.isSandbox == true, load the destination's content from your sandbox backend.
In a UIKit app with scenes, the link arrives as a browsing-web user activity, at launch and while the app runs:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let url = connectionOptions.userActivities.first(where: { $0.activityType == NSUserActivityTypeBrowsingWeb })?.webpageURL {
open(url)
}
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL {
open(url)
}
}3. Handle your URL scheme
If your app has a URL scheme, the preview page's Open the App button opens it with the link's path after the scheme, such as myapp://products/1234. That URL isn't a short link, so don't resolve it: its host is the first path segment, so route it as /products/1234. In SwiftUI it arrives in the same onOpenURL:
if url.scheme == "myapp" {
Router.shared.open(path: "/" + (url.host ?? "") + url.path, query: url.query)
}4. Find the link behind an install
On the first launch after an install, ask POST /api/links/check-deferred for the link the user opened before installing. Send the IANA timezone and the screen in pixels, which is points times the screen's scale:
import UIKit
struct DeferredRequest: Encodable {
struct Device: Encodable {
let screenWidth: Double
let screenHeight: Double
let language: String
}
let timezone: String
let device: Device?
}
extension DeepBlueLinks {
@MainActor
static func checkDeferred() async throws -> ResolvedLink? {
let screen = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.screen
let device = screen.map { screen in
DeferredRequest.Device(
screenWidth: Double(screen.bounds.width * screen.scale),
screenHeight: Double(screen.bounds.height * screen.scale),
language: Locale.preferredLanguages.first ?? "en")
}
let body = DeferredRequest(timezone: TimeZone.current.identifier, device: device)
return try await post("https://dev.thedbl.ink/api/links/check-deferred", body: body)
}
}Call it once your first scene is active, and route like a resolved link when it returns one:
if !UserDefaults.standard.bool(forKey: "checkedDeferredLink") {
UserDefaults.standard.set(true, forKey: "checkedDeferredLink")
if let link = try? await DeepBlueLinks.checkDeferred(), let destination = URL(string: link.longUrl) {
Router.shared.open(path: destination.path, query: destination.query)
}
}A 404 here is normal: most launches don't follow a link. Deferred deep links explains when a match isn't possible.
Test it
- Universal links work on a device, not the Simulator. Paste a short link into Notes and tap it; long-press it to choose between the app and Safari.
- Typing the link into Safari's address bar opens the page, not the app, by design.
- In Settings › Developer › Universal Links, Diagnostics checks a URL against your installed app.
- iOS fetches
apple-app-site-associationthrough Apple's CDN, which can lag behind a change to your app's settings. While testing,applinks:demo.dev.thedbl.ink?mode=developerwith Associated Domains Development turned on fetches it directly.