Допустим, пользователь получил письмо с кнопкой «Посмотреть заказ». Он нажимает — и что происходит? Открывается браузер, показывается страница входа, человек вводит пароль, ищет нужный заказ... Раздражает. А могло быть иначе: нажал — приложение открылось сразу на нужном экране. Это и есть deeplink.

Deeplink — это ссылка, которая ведёт не просто в приложение, а в конкретное место внутри него. На карточку товара, в чат, на экран оплаты. Звучит просто, но под капотом — два разных механизма со своими плюсами, минусами и подводными камнями.

Два способа сделать deeplink в iOS

URL Schemes — старый добрый способ

Custom URL Scheme — это когда приложение регистрирует себе кастомный протокол вместо https. Например, myapp://product/123. iOS видит такую ссылку и знает: её должно открыть конкретное приложение.

Настраивается элементарно. В Info.plist добавляешь:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
  </dict>
</array>

Потом в AppDelegate или через SceneDelegate ловишь вызов:

func application(_ app: UIApplication,
                 open url: URL,
                 options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    // разбираем url и роутим
    return true
}

Звучит хорошо, но есть проблемы.

Первая: схема не уникальна. Любое приложение может зарегистрировать myapp://. Если на устройстве окажется два приложения с одинаковой схемой, iOS выберет одно из них — и это не обязательно твоё. На практике крупные компании бронируют схемы вроде fb:// или twitter://, но никаких гарантий нет.

Вторая: если приложение не установлено, URL Scheme просто падает. Браузер показывает ошибку, пользователь недоумевает. Никакого фоллбэка нет из коробки.

Третья: в Safari с iOS 9 нельзя просто вызвать myapp:// через JavaScript — браузер покажет алерт с вопросом. Это раздражает пользователя.

URL Schemes до сих пор используют для межприложечной коммуникации — когда одно твоё приложение открывает другое твоё. Но для связки сайт-приложение лучше Universal Links.

Universal Links — современный подход

Universal Links работают иначе. Ты берёшь обычную HTTPS-ссылку — например, https://myapp.ru/product/123 — и говоришь iOS: «Если это приложение установлено, открой ссылку в нём. Если нет — открой в браузере».

Плюсы очевидны:

  • Одна ссылка работает везде — в приложении, в браузере, в письме
  • Если приложение не установлено, человек попадает на сайт (нормальный фоллбэк)
  • Схему нельзя «угнать», потому что ты подтверждаешь владение доменом
  • Работает без алертов

Настройка сложнее, но один раз — и работает надёжно.

Как настроить Universal Links

Шаг 1: файл apple-app-site-association

На сервере, в корне домена или по пути /.well-known/, нужно разместить файл apple-app-site-association (без расширения). iOS запрашивает его при установке приложения.

Содержимое файла:

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.yourcompany.yourapp",
        "paths": [
          "/product/*",
          "/order/*",
          "/user/profile"
        ]
      }
    ]
  }
}

appID — это Team ID из Apple Developer + bundle identifier приложения. Найти Team ID можно в developer.apple.com в разделе Membership.

paths — паттерны URL, которые должны открываться в приложении. Звёздочка — wildcard. Можно исключать пути через NOT:

"paths": [
  "/product/*",
  "NOT /product/sale"
]

Важный момент: файл должен отдаваться с Content-Type: application/json и быть доступен по HTTPS без редиректов. iOS не пойдёт по редиректам при его загрузке.

С iOS 13 Apple перешла на CDN — они кешируют файл сами. Это значит, что изменения применяются не мгновенно. Обновление может занять от нескольких часов до суток.

Шаг 2: настройка в Xcode

В Xcode открываешь Target → Signing & Capabilities → добавляешь Associated Domains. Туда вписываешь:

applinks:myapp.ru

Если нужна поддержка нескольких доменов — добавляешь несколько строк:

applinks:myapp.ru
applinks:www.myapp.ru

Xcode автоматически добавит нужные entitlements в сборку.

Шаг 3: обработка в приложении

В SceneDelegate:

func scene(_ scene: UIScene,
           continue userActivity: NSUserActivity) {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else { return }
    
    handleDeeplink(url: url)
}

Если приложение открывается с нуля (не из фона):

func scene(_ scene: UIScene,
           willConnectTo session: UISceneSession,
           options connectionOptions: UIScene.ConnectionOptions) {
    if let userActivity = connectionOptions.userActivities.first,
       userActivity.activityType == NSUserActivityTypeBrowsingWeb,
       let url = userActivity.webpageURL {
        handleDeeplink(url: url)
    }
}

Сама функция роутинга — это уже твоя логика. Можно парсить URL вручную или использовать библиотеки вроде URLNavigator.

Роутинг: как правильно разбирать ссылки

Простой вариант — switch по path:

func handleDeeplink(url: URL) {
    let components = URLComponents(url: url, resolvingAgainstBaseURL: true)
    let path = url.path
    
    switch path {
    case let p where p.hasPrefix("/product/"):
        let id = String(p.dropFirst("/product/".count))
        showProduct(id: id)
    case "/cart":
        showCart()
    case "/profile":
        showProfile()
    default:
        showHome()
    }
}

Для более сложных случаев лучше использовать паттерн Router или Coordinator, чтобы не захламлять AppDelegate. Deeplink должен уметь открыть нужный экран из любого состояния приложения — даже если пользователь был на другом разделе.

Частая ошибка: разработчик делает deeplink только для «свежего» запуска приложения, но забывает про кейс, когда приложение уже открыто. Тогда scene(_:continue:) не вызывается — нужно отдельно обрабатывать scene(_:openURLContexts:) для URL Schemes или использовать NSUserActivity через application(_:continue:restorationHandler:) в старом AppDelegate-подходе.

Как связать сайт с приложением

На сайте всё просто: используешь обычные HTTPS-ссылки. Если приложение установлено — iOS перехватит их. Если нет — человек попадёт на сайт.

Но часто нужно сделать умную кнопку «Открыть в приложении» или баннер. Для этого есть несколько подходов.

Smart App Banners — встроенный механизм iOS. В <head> страницы добавляешь:

<meta name="apple-itunes-app" 
      content="app-id=123456789, app-argument=https://myapp.ru/product/123">

Safari покажет баннер сверху с кнопкой «Открыть» или «Загрузить». Выглядит нативно, не требует JavaScript. app-argument — это deeplink, который будет передан в приложение.

JavaScript + проверка — более гибкий вариант. Можно попробовать открыть URL Scheme через window.location, и если через 1-2 секунды пользователь всё ещё на странице — значит, приложение не установлено, перенаправляем в App Store. Это хак, который работает, но не всегда надёжно.

Branch.io, Firebase Dynamic Links — готовые сервисы для deferred deeplinks. Они решают одну важную проблему: что если пользователь нажал ссылку, но приложение ещё не установлено? После установки нужно открыть тот же экран. Это называется deferred deeplink — отложенный deeplink.

Firebase Dynamic Links, к сожалению, были закрыты в 2025 году. Сейчас популярные альтернативы — Branch.io и Appsflyer. Они создают «умные» ссылки, которые:

  • Определяют платформу (iOS/Android/desktop)
  • Сохраняют параметры до установки приложения
  • После установки «раскрывают» отложенный deeplink
  • Дают аналитику по переходам

Интеграция с внешними сервисами

Deeplinks нужны не только для сайта. Вот типичные сценарии:

Push-уведомления — самый частый кейс. В payload уведомления передаёшь deeplink:

{
  "aps": {
    "alert": "Ваш заказ отправлен"
  },
  "deeplink": "https://myapp.ru/order/456"
}

В обработчике уведомления достаёшь этот URL и роутишь. Пользователь нажимает на уведомление — попадает сразу на заказ.

Email-рассылки — вставляешь Universal Link в письмо. Большинство почтовых клиентов на iOS открывают ссылки через Safari/браузер, и Universal Links работают. Исключение — некоторые клиенты открывают ссылки во встроенном WebView, и там Universal Links могут не сработать.

QR-коды — генерируешь QR с Universal Link. Стандартная камера iOS умеет их открывать прямо в приложении, без перехода в браузер.

Межприложечная интеграция — если нужно, чтобы другое приложение открыло твоё, можно использовать как URL Schemes (если договорились о схеме), так и Universal Links (если у тебя есть домен).

Частые ошибки и как их избежать

Файл apple-app-site-association недоступен или отдаёт редирект. iOS не пойдёт по редиректам. Проверить можно через curl -I https://yourdomain.ru/apple-app-site-association — должен быть 200, Content-Type: application/json.

Неправильный Team ID. Встречается часто. Team ID — это 10-значный идентификатор вроде A1B2C3D4E5, не путай с App ID Prefix.

Пути в AASA не совпадают с реальными URL. Если в файле написано /product/*, а реальные ссылки идут как /products/123 — не работает. Паттерны чувствительны к точности.

Обработка только при холодном старте. Обязательно проверь, что deeplink работает, когда приложение уже запущено в фоне.

Отсутствие фоллбэка. Если Universal Link не сработал (например, нет интернета при установке, и AASA не загрузился), ссылка откроется в браузере. Убедись, что страница на сайте нормально отображается и содержит кнопку «Открыть в приложении».

Тестирование

Проверять deeplinks удобно несколькими способами:

Через Notes — напиши ссылку в заметках, нажми долгим тапом, выбери «Открыть ссылку». Быстро и без лишних инструментов.

Через Safari на устройстве — просто открой URL.

Через терминал с симулятором:

xcrun simctl openurl booted "https://myapp.ru/product/123"

Через Xcode — Debug → Simulate Location / Push Notifications не поможет для deeplinks, но можно добавить launch argument:

-deeplink https://myapp.ru/product/123

И в коде в didFinishLaunching проверять этот аргумент — удобно для разработки.

Для проверки самого файла AASA есть официальный валидатор от Apple — ищи по запросу «AASA validator». Он проверит формат и доступность файла.

Когда это особенно важно для бизнеса

Deeplinks — не техническая деталь, это прямо влияет на конверсию. Исследования показывают, что пользователи, попавшие в нужное место приложения сразу, конвертируются в 2-3 раза лучше, чем те, кого бросили на главный экран.

Для интернет-магазинов это критично: человек видит товар в Instagram, нажимает ссылку, попадает на карточку товара в приложении с уже добавленным в корзину продуктом — это совсем другая история, чем «попади на главную и поищи сам».

Если ты заказываешь разработку мобильного приложения или интернет-магазина — спроси подрядчика заранее, как будут реализованы deeplinks и deferred deeplinks. Это должно быть частью проекта с самого начала, а не «доделаем потом». В REEXY, например, интеграцию deeplinks закладывают в техническое задание ещё на этапе проектирования, чтобы не переделывать архитектуру роутинга на середине разработки.

Что проверить перед релизом

  • Файл AASA доступен на всех поддоменах, которые нужны
  • Все паттерны путей прописаны и протестированы
  • Роутер обрабатывает все возможные deeplink-URL, включая невалидные (без краша)
  • Deeplink работает при холодном старте и при переходе из фона
  • Фоллбэк на сайте работает корректно
  • Push-уведомления с deeplink протестированы на реальном устройстве
  • Deferred deeplink (если нужен) работает после чистой установки

Deeplinks — один из тех механизмов, который не виден пользователю напрямую, но сильно влияет на его опыт. Сделал правильно — люди не замечают и просто пользуются. Сделал плохо — получаешь негативные отзывы и потери в конверсии.