Реалізація 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 тижнів. Вартість розраховується після аналізу користувацьких сценаріїв.







