Handoff Implementation Between iOS Devices
Handoff allows you to start a task on one Apple device and continue on another—text in Notes, a page in Safari, a screen in your application. The application icon appears in the Dock on Mac or in App Switcher on another iPhone/iPad. The user taps—your application opens in the same state.
It works through Bluetooth LE (device discovery) + iCloud (payload transfer). Both devices must be signed in with the same Apple ID.
NSUserActivity — The Only API
Handoff is built on NSUserActivity. The same class used for Spotlight and Siri Shortcuts—this is intentional, Apple's unified Activity architecture.
// On the sending device
let activity = NSUserActivity(activityType: "com.yourapp.editDocument")
activity.title = document.title
activity.isEligibleForHandoff = true
activity.userInfo = ["documentId": document.id, "scrollPosition": scrollOffset]
activity.needsSave = true // requests userActivityWillSave before transfer
self.userActivity = activity
activity.becomeCurrent()
activityType is a string from the NSUserActivityTypes array in Info.plist. If the type is not registered—Handoff doesn't work.
needsSave = true and userActivityWillSave. If state changes in real time (scroll position, entered text), don't update userInfo on every change—it's expensive. Set needsSave = true, the system calls userActivityWillSave(_ activity:) before sending. Update userInfo there.
Receiving on Another Device
// AppDelegate or 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
}
// Open the needed screen
navigationController.pushViewController(DocumentViewController(id: documentId), animated: false)
return true
}
In SceneDelegate (iOS 13+): scene(_:willConnectTo:options:) for new launch and scene(_:continue:) for already running application. Handle both cases.
What to Pass in userInfo
userInfo is limited: Property list types only (String, Int, Data, Array, Dictionary). Don't try to serialize NSManagedObject—it crashes. Maximum payload size—a few kilobytes. For large state: pass the identifier, load from iCloud or local cache on the receiving side.
Continuation stream. For files, there's NSUserActivity.addUserInfoEntries(from:) + transferUserInfoCompletionHandler. For actual data streaming—continuation stream through getContinuationStreams(). But this is rare—usually ID + reload is enough.
Typical Mistakes
becomeCurrent() not called. Without it, Handoff doesn't activate. Call in viewDidAppear, not in viewDidLoad.
invalidate() not called when leaving screen. If the activity isn't invalidated, the Handoff icon remains on other devices even when the task is complete. In viewDidDisappear or deinit: activity.invalidate() or self.userActivity = nil.
activityType mismatch between versions. If a new app version renames activityType, old devices receive an unknown type and Handoff silently fails. Version your activityType or handle old types.
Mac Catalyst and macOS
On Mac Catalyst, the same NSUserActivity API. Handoff works between iOS and macOS if the app exists on both platforms. For macOS AppKit—NSApplicationDelegate.application(_:continue:restorationHandler:).
Timeline
Basic Handoff implementation for 1–3 activity types: 1–2 weeks. Full integration with complex state, multiple screens, edge cases handling (no data, no network): 3–5 weeks. Cost is calculated after analyzing user scenarios.







