Implementing Task Handoff Across Devices in Mobile Applications
User starts filling a form on phone, opens laptop—the form appears with the same data at the same step. This is Handoff, and Apple has it out-of-the-box via NSUserActivity. For cross-platform and cross-ecosystem scenarios (iOS → Android, mobile → desktop), build your own.
Apple Handoff: When Nothing Needs Implementation
NSUserActivity—mechanism for transferring task context between Apple devices via iCloud. Supports iOS → macOS, macOS → iOS, iOS → iPadOS.
In React Native via native module:
// iOS Native Module
let activity = NSUserActivity(activityType: "com.yourapp.editDocument")
activity.title = "Editing document"
activity.userInfo = ["documentId": documentId, "scrollPosition": scrollY, "formData": formData]
activity.isEligibleForHandoff = true
activity.becomeCurrent()
Receiving side on macOS/iOS, AppDelegate gets application(_:continue:restorationHandler:) with this NSUserActivity. Data in userInfo—dictionary, limited to ~256 KB.
Handoff limitations: Apple ecosystem only, single authenticated account, Bluetooth + Wi-Fi required. For users with Android phone and Windows laptop—doesn't work.
Cross-Platform Handoff: Server-Side Session
Universal approach: task session stored on server, tied to account, not device.
type TaskSession = {
sessionId: string;
userId: string;
taskType: 'checkout' | 'editDocument' | 'formFill' | 'mediaUpload';
state: Record<string, unknown>; // serialized task state
activeDeviceId: string;
updatedAt: number;
expiresAt: number;
};
Key points:
Granular state: don't save entire Redux store. Save only what's needed to restore task. For form: { step: 2, fields: { name: 'John', email: 'john@...' }, validationState: {...} }. Never save sensitive data (CVV, PIN).
Conflict resolution: user works on two devices simultaneously. Last write wins (LWW)—sufficient for most cases. For complex tasks—use vector clocks.
Active session discovery: on app open, check incomplete sessions and offer to continue.
Auto-Save Task State
const useTaskHandoff = (taskType: string) => {
const [sessionId] = useState(() => generateSessionId());
const debouncedSave = useMemo(
() => debounce(async (state: Record<string, unknown>) => {
await api.handoff.saveSession({
sessionId,
taskType,
state,
deviceId: getDeviceId(),
});
}, 1000),
[sessionId, taskType]
);
// Call on every state change
useEffect(() => {
debouncedSave(currentTaskState);
return () => debouncedSave.cancel();
}, [currentTaskState]);
// Restore on startup
const restoreSession = useCallback(async (incomingSessionId: string) => {
const session = await api.handoff.getSession(incomingSessionId);
if (session) restoreTaskState(session.state);
}, []);
return { restoreSession };
};
Debounce at 1 second—don't save on every keystroke. Active typing = 60+ requests per minute, unnecessary for server and battery.
Deep Link to Open Task on Another Device
How does the second device learn about session? Options:
-
Push notification: when device goes inactive, send silent push with
sessionId. Second device shows banner "Continue on this device?". -
Deep link: user explicitly copies link
yourapp://handoff/session/abc123and opens on other device. -
QR code: show QR with
sessionId, scan with second device. - Automatically: on app open on second device—query active sessions and offer list of incomplete tasks.
Option 4—best UX, but requires clear privacy policy: user should see incomplete tasks are stored on server and have ability to delete them.
Typical Tasks for Handoff
- Multi-page forms (checkout, surveys, applications)
- Document/post editing with intermediate state
- Media upload (started on phone, continue on tablet with larger screen)
- Game sessions with progress saved across devices
Don't implement Handoff for: single-screen tasks, tasks without clear "completion step", tasks with sensitive data without E2E encryption.
Estimate
Server-side task session with deep link, auto-save, and recovery UI: 3–5 weeks. With Apple NSUserActivity + cross-platform fallback: 4–6 weeks.







