Continuing User Activities with Handoff
At a glance
| Item | Summary |
|---|---|
| Purpose | Continue a viewed map region or an open store-detail interaction between iOS and macOS. |
| Architecture | Parallel UIKit/AppKit controllers share store models, search logic, an initial-region state enum, and an NSUserActivity encoding extension. |
| Handoff boundary | Activities carry stable region and store URL data; the receiving controller reconstructs the map and waits for matching search results before presenting detail. |
Project structure
HandoffMapViewer/
├── Shared sources/
│ ├── AppleStore.swift
│ ├── AppleStoreDirectory.swift
│ ├── InitialMapRegionState.swift
│ └── NSUserActivity+HandoffMapViewer.swift
├── HandoffMapViewerIOS/*ViewController.swift
└── HandoffMapViewerMac/*ViewController.swift
Shared files hold portable state and serialization. Platform folders contain near-parallel presentation and lifecycle adapters.
Overall architecture
flowchart LR
IOS["iOS MapViewController"] --> Activity["NSUserActivity codec"]
Mac["macOS MapViewController"] --> Activity
Activity --> Handoff["system Handoff"]
Handoff --> Activity
IOS --> Directory["AppleStoreDirectory"]
Mac --> Directory
Directory --> Search["MKLocalSearch"]
Directory --> Store["AppleStore set"]
Reference code
HandoffMapViewer/Shared sources/NSUserActivity+HandoffMapViewer.swift:38 — shared code creates the viewing activity and defines its required payload contract.
static func initForRegionViewing() -> NSUserActivity {
let activity = NSUserActivity(activityType: viewingActivityType)
activity.title = NSLocalizedString("Viewing Map", comment: "Viewng Map")
activity.requiredUserInfoKeys = viewingActivityRequiredKeys
activity.isEligibleForHandoff = true
return activity
}Ownership and state
classDiagram
MapViewController *-- CLLocationManager
MapViewController *-- AppleStoreDirectory
MapViewController *-- NSUserActivity : viewing
MapViewController *-- NSUserActivity : editing
AppleStoreDirectory *-- Set : stores
AppleStoreDirectory *-- Closure : onStoresFound
MapViewController *-- InitialMapRegionState
Ownership evidence
HandoffMapViewer/HandoffMapViewerIOS/MapViewController.swift:21 — the screen owns search, activity, and restoration state.
class MapViewController: UIViewController {
// ...
@IBOutlet private weak var mapView: MKMapView!
// ...
}Each platform controller is a feature owner. AppleStoreDirectory owns its deduplicated store set and iCloud favorite persistence. The system owns activity transfer; the receiving controller owns reconstruction and the state that prevents current-location startup from racing Handoff.
Class and protocol design
| Type | Responsibility |
|---|---|
AppleStore |
Validated, hashable store value derived from MKMapItem. |
AppleStoreDirectory |
Search, deduplicate, callback, and favorite persistence. |
NSUserActivity extension |
Define activity types, keys, constructors, encoders, and decoders. |
InitialMapRegionState |
Model startup/restoration progress explicitly. |
Platform MapViewController types |
Adapt UIKit/AppKit, MapKit, location, popover, and activity lifecycle. |
The sample relies on framework delegates rather than an app-defined cross-platform controller protocol; source sharing occurs below the UI layer.
Access control
| Boundary | Effect and rationale |
|---|---|
| Private activity payload keys | Only the codec extension can read/write the wire representation. |
| Public activity-type constants | Platform targets can identify activity categories; the rest of the shared types remain app-internal. |
| Private controller managers, activities, and restoration state | Keeps lifecycle transitions within one screen owner. |
| Private directory store set and persistence helpers | Forces discovery and favorites through validated methods. |
| Weak/self-aware callback capture | Avoids the asynchronous search owning the directory/controller indefinitely. |
HandoffMapViewer/Shared sources/NSUserActivity+HandoffMapViewer.swift:24 shows private payload keys; HandoffMapViewer/Shared sources/AppleStoreDirectory.swift:16 shows private store persistence state.
Logic ownership and placement
| Logic | Owner |
|---|---|
| Activity payload schema | NSUserActivity extension |
| Store validation and identity | AppleStore |
| Search, deduplication, favorites | AppleStoreDirectory |
| Map, popover, activity switching | Platform MapViewController |
| Startup race and pending detail restoration | InitialMapRegionState |
Design patterns
| Pattern | Evidence | Purpose |
|---|---|---|
| Activity codec | HandoffMapViewer/Shared sources/NSUserActivity+HandoffMapViewer.swift:16 |
Centralizes the cross-device payload contract. |
| State machine enum | HandoffMapViewer/Shared sources/InitialMapRegionState.swift:10 |
Distinguishes unset, local, viewing, and pending-store startup paths. |
| Callback | HandoffMapViewer/Shared sources/AppleStoreDirectory.swift:24 |
Reports newly discovered stores without knowing the map UI. |
| Cross-platform shared model | HandoffMapViewer/Shared sources/AppleStore.swift:16 |
Keeps identity and validation consistent between apps. |
| Framework delegates | HandoffMapViewer/HandoffMapViewerIOS/MapViewController.swift:134 |
Integrates MapKit, location, popover, and activity events. |
Naming conventions
- Activity types use
viewingandstoreEditing, matching the user action rather than a screen class. - Serialization helpers pair verbs:
updateViewingRegion/viewingRegionandupdateEditing/editingStoreURL. - Platform folders repeat controller names intentionally because they are alternative target implementations.
Architecture takeaways
- Put the Handoff payload schema in one shared codec boundary.
- Transfer stable identifiers and reconstruct live framework objects on the receiver.
- Model startup/restoration phases explicitly to avoid racing default navigation.
- Share models and codecs across platforms while keeping UI lifecycle code native to each target.
Source map
| Source | Role |
|---|---|
HandoffMapViewer/Shared sources/NSUserActivity+HandoffMapViewer.swift:16 |
Handoff codec |
HandoffMapViewer/Shared sources/AppleStore.swift:16 |
Store value model |
HandoffMapViewer/Shared sources/AppleStoreDirectory.swift:14 |
Search and favorites owner |
HandoffMapViewer/HandoffMapViewerIOS/MapViewController.swift:11 |
UIKit integration |
HandoffMapViewer/HandoffMapViewerMac/MapViewController.swift:11 |
AppKit integration |