Реализация Handoff между iOS-устройствами
Handoff позволяет начать задачу на одном устройстве Apple и продолжить на другом — текст в Notes, страница в Safari, экран в вашем приложении. Значок приложения появляется в Dock на Mac или в App Switcher на другом iPhone/iPad. Пользователь тапает — открывается ваше приложение в том же состоянии.
Работает через Bluetooth LE (обнаружение устройств) + iCloud (передача payload). Оба устройства должны быть авторизованы в одном Apple ID.
NSUserActivity — единственный API
Handoff строится на NSUserActivity. Тот же класс, что используется для Spotlight и Siri Shortcuts — это не случайность, это единая Activity архитектура Apple.
// На отправляющем устройстве
let activity = NSUserActivity(activityType: "com.yourapp.editDocument")
activity.title = document.title
activity.isEligibleForHandoff = true
activity.userInfo = ["documentId": document.id, "scrollPosition": scrollOffset]
activity.needsSave = true // запрашивает userActivityWillSave перед передачей
self.userActivity = activity
activity.becomeCurrent()
activityType — строка из NSUserActivityTypes массива в Info.plist. Если тип не зарегистрирован — Handoff не работает.
needsSave = true и userActivityWillSave. Если состояние меняется в реальном времени (позиция скролла, введённый текст), не обновляйте userInfo при каждом изменении — это дорого. Установите needsSave = true, система вызовет userActivityWillSave(_ activity:) перед отправкой. Обновляйте userInfo там.
Получение на другом устройстве
// AppDelegate или SceneDelegate
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard userActivity.activityType == "com.yourapp.editDocument",
let documentId = userActivity.userInfo?["documentId"] as? String else {
return false
}
// Открываем нужный экран
navigationController.pushViewController(DocumentViewController(id: documentId), animated: false)
return true
}
В SceneDelegate (iOS 13+): scene(_:willConnectTo:options:) для нового запуска и scene(_:continue:) для уже запущенного приложения. Оба случая нужно обрабатывать.
Что передавать в userInfo
userInfo ограничен: Property list типы только (String, Int, Data, Array, Dictionary). Не пытайтесь сериализовать NSManagedObject — крашится. Максимальный размер payload — несколько килобайт. Для большого состояния: передаём идентификатор, на принимающей стороне загружаем из iCloud или локального кэша.
Continuation stream. Для файлов есть NSUserActivity.addUserInfoEntries(from:) + transferUserInfoCompletionHandler. Для реального стриминга данных — continuation stream через getContinuationStreams(). Но это редкий кейс — обычно достаточно ID + дозагрузки.
Типичные ошибки
becomeCurrent() не вызван. Без него Handoff не активируется. Вызываем в viewDidAppear, не в viewDidLoad.
Не вызван invalidate() при уходе с экрана. Если активность не инвалидировать, Handoff-иконка остаётся на других устройствах даже когда задача завершена. В viewDidDisappear или deinit: activity.invalidate() или self.userActivity = nil.
Несоответствие activityType между версиями. Если в новой версии приложения переименовали activityType, старые устройства получают неизвестный тип и Handoff молча падает. Версионируйте activityType или обрабатывайте старые типы.
Mac Catalyst и macOS
На Mac Catalyst тот же NSUserActivity API. Handoff работает между iOS и macOS если приложение есть на обеих платформах. Для macOS AppKit — NSApplicationDelegate.application(_:continue:restorationHandler:).
Сроки
Базовая реализация Handoff для 1–3 типов активностей: 1–2 недели. Полная интеграция со сложным состоянием, несколькими экранами, обработкой edge cases (нет данных, нет сети): 3–5 недель. Стоимость рассчитывается после анализа пользовательских сценариев.







