Skip to content
macOSDeep Dive Published Updated 7 min readViews unavailable

NSUserActivity on macOS: Handoff State, Continuation, and Recovery

Represent resumable Mac work with versioned NSUserActivity state, minimal handoff payloads, validated continuation, and fallback restoration paths.

NSUserActivity represents a user task that an app can restore, advertise, or continue on another device. It is not a snapshot of the entire process and not a remote object graph. The activity should describe the smallest state needed to resume meaningful work: an activity type, a user-readable title, and a versioned set of property-list-compatible values that identify the current task.

Handoff uses this activity representation to make an eligible task available to another device or app. The receiving app reconstructs its own model and UI. It does not receive the original process, open file handles, database context, or in-memory view controller. Treat continuation as a fresh request to resolve current state, not as a serialized continuation of the source process.

Define activity types and payloads

Declare the activity types your app supports in its information property list and use the exact same stable identifiers when creating activities. A type should name a product action, not a transient view class. Set a localized title so the activity is understandable in system surfaces where it may appear. Include a schema version in userInfo and keep the payload limited to property-list-compatible values.

Apple’s Handoff guidance recommends keeping userInfo small, under 3 KB, and using continuation streams for larger transfers. Do not include the document itself or a large application database snapshot. Send a stable record ID, an optional revision or position, and enough context to resolve the activity after the receiving app launches. If the data has changed or disappeared, show a recoverable state rather than applying stale values.

import Foundation

func makeActivity(documentID: UUID, page: Int) -> NSUserActivity {
    let activity = NSUserActivity(activityType: "com.example.reader.open-document")
    activity.title = "Reading document"
    activity.isEligibleForHandoff = true
    activity.requiredUserInfoKeys = ["schemaVersion", "documentID", "page"]
    activity.userInfo = [
        "schemaVersion": 1,
        "documentID": documentID.uuidString,
        "page": page
    ]
    return activity
}

The activity describes intent and a locator, not proof that the document still exists or that the receiving device can access it. Before sharing the activity, verify that the app can restore it locally and define whether the receiving device needs an account, network access, or an explicit user choice to open the target.

Keep the activity current and owned

An activity associated with a responder represents the task that responder manages. Update its state as the user navigates or edits. updateUserActivityState(_:) lets the app add the latest minimal state when the system asks for it. Mark the activity eligible only while the task is meaningful to continue, and invalidate it when the user closes or completes that task.

Do not create a new activity object for every pointer movement or keystroke. Keep one current activity per logical task, update its state at useful boundaries, and avoid publishing stale identifiers after a document switches. If the user has several windows, each window may represent a different task; activity ownership should follow the active document or controller rather than one app-global mutable variable.

For searchable activities, NSUserActivity can also contribute metadata to system search. Search eligibility and Handoff eligibility are distinct decisions. Do not expose private titles or descriptions in search merely because an activity already exists for continuation. Use the minimum metadata the feature should make discoverable.

Receive and validate continuation

On macOS, the app delegate receives a continuation callback when a user activity launches or resumes the app. Validate the activityType, payload version, required keys, and value types before opening anything. Return whether the app actually handled the activity. If the document ID is invalid or access is unavailable, present a useful error and preserve the rest of the app’s normal launch path.

Do not force-cast payload values or trust a string to be a filesystem path. Resolve IDs through the app’s canonical store and apply current access checks. If the receiving app has to fetch data, keep the UI responsive and show a loading or offline state. A successful continuation callback means the app accepted the activity request; it does not prove that a remote document has loaded or that the user can edit it.

The restoration handler is for restoring relevant view controllers in UIKit-based flows; macOS uses its NSApplicationDelegate continuation methods and AppKit’s own window/document lifecycle. Keep platform-specific restoration code separate rather than copying a UIKit sample directly into an AppKit app.

Fallbacks and conflict behavior

The user can launch the receiving app without Handoff, the source device can go offline, the activity can expire, or the target can be deleted. The app must still open normally from its Dock icon, Finder, or another URL. Do not make the application depend on a Handoff payload being present at startup.

If the task represents an in-progress edit, decide how to handle two devices continuing the same document. The activity is not a lock. It does not prevent edits elsewhere or resolve conflicts. Reopen the current document revision, merge through the app’s normal collaboration or file-version policy, and make any conflict visible when necessary.

Keep Handoff restoration separate from local window restoration. A locally restored window can point to a document that already exists on this Mac, while a continued activity can ask the app to locate or fetch work from another device. Both paths may use the same document ID, but they should have separate error messages and ownership. If the app receives both a launch URL and an activity, define which source of intent wins and record that decision in the navigation coordinator rather than opening two copies of the same document.

Cross-platform continuation requires the corresponding apps and developer identity configuration described by Apple. Test the actual installed app identifiers, Team ID, activity type declarations, and signing setup in the distribution configuration. A simulator callback alone does not prove the end-to-end user-facing Handoff path works across devices.

Privacy and payload minimization

Activities can surface in system UI and cross a device boundary. Avoid confidential text, credentials, access tokens, full document contents, and unnecessary location in userInfo. Prefer opaque app record identifiers whose meaning is resolved only after the receiving app’s normal authorization and data-loading path runs. The payload should be useful enough to route the task but not powerful enough to bypass ordinary access controls.

If a task cannot be safely resumed elsewhere, do not mark it eligible for Handoff. For a user-selected local file that is not available on the receiving Mac, offer a way to locate or transfer it rather than assuming a path is shared. Document bookmark data or a continuation stream may be appropriate only when the underlying product and framework provide a supported cross-device access model.

Observability and testing

Log activity type, schema version, source state revision, continuation outcome, and restore duration. Avoid logging raw userInfo or document content. Distinguish failure to continue from a valid activity whose target is missing; those failures require different remediation.

For development, add a diagnostic screen or structured trace that displays the activity type, parsed schema version, and resolution outcome without exposing the payload’s private fields. This makes cross-device failures easier to separate into activity-not-advertised, activity-not-received, unsupported-version, target-not-found, and data-fetch errors. Retain the trace ID across the handoff if the product has a privacy-reviewed telemetry system, but do not treat a successful advertisement as proof another device received it.

Test create, update, invalidate, handoff to a second Mac, app already running, app not running, unsupported activity type, malformed payload, older schema, deleted target, offline target, and simultaneous edits on two devices. Verify that the normal app launch flow remains available when continuation fails and that no stale activity reopens a previously closed document.

The stable design models a user task with minimal versioned state, updates and invalidates it with task ownership, and resolves it through the receiving app’s ordinary model lifecycle. Handoff transports a continuation request; it does not transport application correctness.

Related:

Sources:

Comments