Deeplink в iOS — это ссылка, которая открывает конкретный экран внутри приложения, а не просто запускает его. Нажал на баннер — попал сразу на акционный товар. Перешёл по письму — оказался в нужном разделе профиля. Звучит просто, но под капотом — несколько механизмов, у каждого свои плюсы и ограничения.
Два способа сделать deeplink
В iOS исторически сложилось два подхода: URL-схемы и универсальные ссылки. Они работают по-разному, и выбор между ними зависит от задачи.
URL-схемы: быстро, но с оговорками
URL-схема — это кастомный протокол вида myapp://product/123. Вы регистрируете схему в Info.plist, и iOS знает: если кто-то открывает ссылку с таким префиксом — запускать ваше приложение.
Настроить просто. В Info.plist добавляете:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
После этого ссылка myapp://product/123 будет открывать приложение. Обрабатывать переход нужно в SceneDelegate:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
handleDeeplink(url: url)
}
Или в AppDelegate, если scene lifecycle не используется:
func application(_ app: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
handleDeeplink(url: url)
return true
}
Проблема URL-схем в том, что их никто не контролирует. Любое другое приложение может зарегистрировать ту же схему. Если два приложения претендуют на myapp://, iOS выберет одно из них — и не факт, что ваше. Это создаёт риски: в теории злоумышленник может перехватить переход и получить данные из ссылки.
Ещё один минус: если приложение не установлено, ссылка просто не откроется. Никакого фолбэка на сайт — пользователь увидит ошибку или ничего не произойдёт.
Универсальные ссылки: надёжно и безопасно
Universal Links появились в iOS 9. Идея в том, что deeplink — это обычный HTTPS-адрес вашего сайта. Например, https://example.com/product/123. iOS проверяет, установлено ли приложение, которое заявило права на этот домен. Если да — открывает приложение. Если нет — открывает сайт в браузере.
Это решает сразу две проблемы: нет конфликтов схем и есть фолбэк для пользователей без приложения.
Как настроить Universal Links
Настройка состоит из двух частей: серверной и клиентской.
На сервере
Нужно разместить файл apple-app-site-association по адресу:
https://example.com/.well-known/apple-app-site-association
Файл без расширения, в формате JSON:
{
"applinks": {
"details": [
{
"appIDs": ["TEAMID.com.example.myapp"],
"components": [
{ "/": "/product/*" },
{ "/": "/profile/*" },
{ "/": "/order/*" }
]
}
]
}
}
appIDs — это Team ID из Apple Developer Portal плюс Bundle Identifier. components — маски URL, которые будут открывать приложение. Формат с components появился в iOS 14 и даёт более гибкую настройку, чем старый paths.
Важно: файл должен отдаваться с Content-Type: application/json, без редиректов, по HTTPS. Apple проверяет его при установке приложения и периодически в фоне.
В приложении
В Xcode в настройках таргета нужно включить Associated Domains и добавить:
applinks:example.com
Обработка перехода — в SceneDelegate:
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else { return }
handleDeeplink(url: url)
}
Или в AppDelegate:
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard let url = userActivity.webpageURL else { return false }
handleDeeplink(url: url)
return true
}
Обработка переходов: как не превратить это в лапшу
Разобрать URL несложно, но с ростом приложения количество маршрутов растёт, и логика быстро превращается в месиво из if-else.
Простой разбор URL
Для небольшого приложения достаточно парсить компоненты вручную:
func handleDeeplink(url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return }
let path = components.path
let queryItems = components.queryItems ?? []
switch path {
case _ where path.hasPrefix("/product/"):
let productId = String(path.dropFirst("/product/".count))
navigateToProduct(id: productId)
case "/profile":
let tab = queryItems.first(where: { $0.name == "tab" })?.value
navigateToProfile(tab: tab)
default:
break
}
}
Роутер для больших приложений
Когда маршрутов становится много, имеет смысл вынести логику в отдельный роутер:
enum DeeplinkRoute {
case product(id: String)
case profile(tab: String?)
case order(id: String)
case unknown
}
struct DeeplinkParser {
func parse(url: URL) -> DeeplinkRoute {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
return .unknown
}
let path = components.path
let query = components.queryItems ?? []
if path.hasPrefix("/product/") {
return .product(id: String(path.dropFirst("/product/".count)))
}
if path == "/profile" {
return .profile(tab: query.first(where: { $0.name == "tab" })?.value)
}
if path.hasPrefix("/order/") {
return .order(id: String(path.dropFirst("/order/".count)))
}
return .unknown
}
}
class DeeplinkRouter {
private let parser = DeeplinkParser()
func handle(url: URL) {
switch parser.parse(url: url) {
case .product(let id):
navigateToProduct(id: id)
case .profile(let tab):
navigateToProfile(tab: tab)
case .order(let id):
navigateToOrder(id: id)
case .unknown:
break
}
}
}
Такой подход держит логику в одном месте и упрощает тестирование — парсер можно покрыть юнит-тестами независимо от UI.
Deeplink при холодном старте
Частая ошибка — не учитывать, что приложение могут открыть через deeplink, когда оно не было запущено. В этом случае нужно дождаться, пока приложение инициализируется, и только потом выполнить навигацию.
Если попытаться сразу показать экран товара, пока ещё не загрузился стек навигации — получите краш или пустой экран.
Простое решение: хранить ожидающий deeplink и обрабатывать его после завершения инициализации:
class DeeplinkRouter {
private var pendingURL: URL?
private var appIsReady = false
func handle(url: URL) {
if appIsReady {
navigate(to: url)
} else {
pendingURL = url
}
}
func appDidBecomeReady() {
appIsReady = true
if let url = pendingURL {
navigate(to: url)
pendingURL = nil
}
}
}
URL-схемы vs Universal Links — что выбрать
Если кратко:
URL-схемы подходят для межприложного взаимодействия, когда оба приложения ваши. Например, корпоративный набор приложений общается между собой. Настраивается за несколько минут, не требует сервера.
Universal Links — стандарт для любого публичного deeplink. Надёжнее, безопаснее, есть фолбэк на сайт. Требует настройки сервера, но это разовая работа.
На практике часто делают и то, и другое: Universal Links — для внешних переходов из писем, мессенджеров и браузера; URL-схемы — для переходов внутри экосистемы своих приложений.
Частые грабли
AASA не обновляется сразу. Apple кэширует файл. При изменении на сервере обновление доходит не сразу — иногда нужно подождать несколько часов или переустановить приложение.
Переход не срабатывает из WKWebView. Внутри WKWebView Universal Links по умолчанию не работают — Apple сделала это намеренно, чтобы не ломать веб-навигацию. Нужно перехватывать навигацию через WKNavigationDelegate и обрабатывать вручную.
Домен с www и без. Если сайт доступен на example.com и www.example.com — нужно настроить Associated Domains для обоих: applinks:example.com и applinks:www.example.com.
Параметры теряются. При обработке ссылки легко забыть про query-параметры или неправильно их декодировать. URLComponents справляется с этим лучше, чем ручная обработка строк.
Нет тестирования deeplinks. Проверить deeplink в симуляторе можно через Terminal:
xcrun simctl openurl booted "https://example.com/product/123"
Добавьте проверку deeplinks в регрессионное тестирование — иначе что-то обязательно сломается в самый неподходящий момент.
Deeplink в push-уведомлениях
Deeplinks часто используют в паре с push-уведомлениями. В пейлоаде уведомления передают URL или идентификатор маршрута:
{
"aps": {
"alert": "Ваш заказ готов"
},
"deeplink": "https://example.com/order/456"
}
При нажатии на уведомление обрабатываете пейлоад в UNUserNotificationCenterDelegate и передаёте URL в роутер. Логика та же, что и для обычных переходов.
Deeplinks — одна из тех вещей, которые кажутся простыми, пока не начинаешь разбираться в деталях. Правильно настроенные, они заметно улучшают пользовательский опыт: меньше шагов до нужного контента, плавные переходы из рекламы и писем, удобное межприложное взаимодействие.
Если разрабатываете мобильное приложение в связке с сайтом — имеет смысл сразу закладывать поддержку Universal Links. В REEXY помогаем настроить и серверную часть, и клиентскую логику: от AASA-файла до роутера в приложении. Подробности и цены на разработку — на сайте r3xy.ru.