Skip to main content
All articles

Continuity Camera and the Features With No API

Ben Van AkenCo-Founder & CTO11 min read
Continuity Camera and the Features With No API

Parts 2 to 7 of this series dealt with the two Continuity members that ask something of your code: Handoff, which you opt into deliberately, and Universal Clipboard, which is already running in your app whether you opted in or not. This part closes the family out. Some of what is left is adoptable in an afternoon. Some of it cannot be adopted at all, and the most useful thing an article can do there is tell you that before you spend a sprint finding out.

Continuity Camera import: one menu item, or nothing

A Mac app can take a photo, a scanned document, a sketch or a marked-up page directly from a nearby iPhone or iPad. The user picks Take Photo, Scan Documents, Add Sketch or Add Markup, the phone wakes into the right capture UI, and the result lands in the Mac app. Continuity Markup and Continuity Sketch are not separate frameworks with their own adoption story: they ride the same menu item.

The opt-in is one menu item, identified by a constant.

Swift
import AppKit

// Built once, while the File menu is assembled.
func addImportFromDevice(to fileMenu: NSMenu) {
    let item = NSMenuItem(
        title: "Import from iPhone or iPad",
        action: nil,
        keyEquivalent: ""
    )
    // The entire opt-in is this line. Leave it
    // out and the submenu never appears, with
    // no warning anywhere.
    item.identifier =
        NSMenuItem.importFromDeviceIdentifier
    fileMenu.addItem(item)
}

The system fills that submenu in for you. What it offers depends on what something in your responder chain says it is able to receive, which is the ordinary Services-menu requestor contract rather than anything Continuity-specific.

Swift
typealias PBType = NSPasteboard.PasteboardType

// Somewhere in the responder chain.
extension EditorViewController:
    NSServicesMenuRequestor {

    override func validRequestor(
        forSendType send: PBType?,
        returnType ret: PBType?
    ) -> Any? {
        if ret == .png || ret == .pdf {
            return self
        }
        return super.validRequestor(
            forSendType: send,
            returnType: ret
        )
    }

    func readSelection(
        from pasteboard: NSPasteboard
    ) -> Bool {
        guard let data = pasteboard.data(
            forType: .png
        ) else { return false }
        insert(imageData: data)
        return true
    }
}

Now the part that matters more than the code. The absence of this menu item is silent. There is no build warning, no runtime log, no deprecation notice, no checklist item in Xcode. A Mac app that never adds it simply never grows the affordance, and nobody on the team is ever told that the affordance was available. That is a different class of failure from every other one in this series: a broken Handoff shows up as something not arriving, and a Universal Clipboard mistake shows up as data on a device it should not be on, but this one shows up as a menu your users never knew they could have had.

It is also the reason so many otherwise excellent Mac apps quietly lack it. Nothing is wrong with them. They just never added the line.

Continuity Camera as a webcam: automatic until you defeat it

The other half of Continuity Camera has nothing to do with menus. It is the iPhone presenting itself to the Mac as a camera. From an app's point of view this one is largely automatic. AVFoundation models it as a device type: AVCaptureDevice.DeviceType.continuityCamera, with an isContinuityCamera property on the device, both available from iOS 17 and macOS 14. On macOS, NSCameraUseContinuityCameraDeviceType is the Info.plist key that opts your app into the new device type.

XML
<key>NSCameraUseContinuityCameraDeviceType</key>
<true/>

There are two ways to defeat it, and both are common enough to be worth naming.

The first is hard-coding builtInWideAngleCamera. An app that asks for exactly that device type, in a discovery session or in a default-device call, will never be handed the iPhone, because the iPhone is not that device type. From the outside it looks like the app does not support Continuity Camera. It supports it fine; it just never asked.

The second is subtler and costs more: building the AVCaptureDevice.DiscoverySession once, reading its devices at launch, and treating that array as the answer for the rest of the process. Devices come and go here in a way they never did with a built-in camera. The iPhone appears when it is nearby, and it disappears when it is carried away or goes to sleep. An app that snapshotted the list at launch shows a camera picker that is wrong within minutes. The expensive case is worse: it keeps pointing a running session at a device that is no longer there when the phone sleeps mid-call.

Swift
import AVFoundation

final class CameraSource {

    // Ask for the iPhone explicitly. A list with
    // only .builtInWideAngleCamera in it can
    // never return one.
    private let discovery =
        AVCaptureDevice.DiscoverySession(
            deviceTypes: [
                .builtInWideAngleCamera,
                .continuityCamera,
                .external
            ],
            mediaType: .video,
            position: .unspecified
        )

    private var watch: NSKeyValueObservation?

    init() {
        // Read on every change, not once.
        watch = discovery.observe(
            \.devices,
            options: [.initial, .new]
        ) { [weak self] session, _ in
            self?.devicesChanged(session.devices)
        }
    }

    private func devicesChanged(
        _ devices: [AVCaptureDevice]
    ) {
        let phone = devices.first {
            $0.isContinuityCamera
        }
        // Offer `phone` when it is present, and
        // reconfigure the running session if the
        // device it was using has gone away.
    }
}

The mechanism you use for the change signal matters less than the fact that there is one. Observing the discovery session's device list, as above, is one way; the connect and disconnect notifications are another. What is not an option is reading the list once and believing it.

Shared with You: structurally not adoptable for private content

Shared with You is the family member where the most valuable finding is a negative one, and where knowing it early saves real money.

The framework is SharedWithYou, available from iOS 16 and macOS 13, and it gives you SWHighlightCenter, SWHighlight and SWAttributionView: a source of highlights, the highlight itself, and the view that credits the person who shared it. Read what a highlight is and the constraint is immediately visible. An SWHighlight is a priority-ordered list of universal links. That is what the framework moves between people.

Swift
import SharedWithYou

final class Highlights: NSObject,
    SWHighlightCenterDelegate {

    private let center = SWHighlightCenter()

    override init() {
        super.init()
        center.delegate = self
    }

    func highlightCenterHighlightsDidChange(
        _ center: SWHighlightCenter
    ) {
        for highlight in center.highlights {
            // A URL. That is the whole payload.
            route(to: highlight.url)
        }
    }
}

There is no mechanism in the framework for authenticated, private content. Content that sits behind a sign-in cannot participate, which covers a workspace, an internal tool, and anything with a tenant boundary. Not partially, not with effort, not with a clever entitlement. There is no surface to implement against, because the thing Shared with You hands the other person is a link their device is expected to be able to resolve on its own.

That makes this a do-not-spend-a-sprint finding rather than a difficulty rating, and it is the kind of thing a planning meeting gets wrong in both directions: teams either budget weeks for an integration that cannot exist, or assume the whole framework is irrelevant to them. The corollary is worth stating too, as reasoning rather than as a documented rule. The part of your content that could participate is whatever genuinely has public universal links, such as a published page or a shared document with a real public URL. Nothing else can.

In the production SwiftUI app this series draws on, which we deliberately do not name, the count of SharedWithYou call sites is zero. For an app whose content lives behind a login, zero is the correct number.

The members with no API, and what each one breaks

Part 1 set out Continuity as three contracts wearing one brand name: opt in with an API, opt out of something already running, or no API at all. This is the third group. You cannot adopt these, you cannot switch them off from inside your app, and there is nothing to test. What you can do is know what they do to a surface you already ship.

  • Sidecar renders your app on an extended display, and that is genuinely all it is. A window that assumes a particular display's scale or characteristics misbehaves on an iPad running Sidecar exactly as it misbehaves on any second monitor. If your layout is already correct on an external display, it is already correct here; if it is not, Sidecar is not the bug.
  • Universal Control routes pointer and keyboard events between machines. It does not route touch, and that distinction is the whole finding. A surface that depends on hover precision becomes harder to use when it is being driven this way, not impossible. Think of a drag handle a few points wide, or an affordance that only appears on hover. Design for harder, and do not assume a touch gesture will arrive.
  • Instant Hotspot and Auto Unlock have no app impact at all. They are in this list so you can stop thinking about them.

iPhone Mirroring: no way to detect it, no way to opt out

iPhone Mirroring deserves its own section, because it is the one no-API member with a real security consequence and because the honest answer is uncomfortable.

Apple documents the posture in Apple Platform Security: during a mirroring session the iPhone stays locked, a persistent notification is shown on its lock screen while the session runs, and a banner appears the next time the phone is unlocked. That is a careful design, and on a personal device it is probably sufficient: the owner of the phone is told, twice.

Apple Platform Security: iPhone Mirroring security

What it does not give an app is anything to call. There is no app-level way to detect that you are being mirrored, and no way to opt a screen out of it. iOS has UIScreen.isCaptured, the property that lets an app redact itself while it is being recorded or mirrored to AirPlay. There is no analogue of it for iPhone Mirroring. The research behind this series went looking for it specifically. It is absent, and an absence that was searched for is a finding rather than a gap.

Swift
// Exists, and covers recording and AirPlay:
if view.window?.screen.isCaptured == true {
    redactSensitiveFields()
}

// Does not exist, at any spelling: no
// isMirroredToMac, no notification, no
// entitlement that asks to be told, no key
// that opts one screen out.

For most apps this is a non-event. Sensitive surfaces are the problem. A balance, a medical record, a customer's data inside a tenanted workspace: for those, this is an exposure that cannot be mitigated in application code today. We are saying that plainly rather than gesturing at a workaround, because any workaround an article offered here would be invented.

The only lever that exists is a managed-device one, and it does not belong to you. allowiPhoneMirroring is an MDM restriction applied to a managed Mac by the customer's administrator, and it is reported as allowed by default. Two caveats before that sentence goes into a security questionnaire answer. First, the key is MDM-vendor sourced rather than confirmed verbatim from Apple's own key table; Apple's live Restrictions page did not render its full table in either research fetch. Second, the only Continuity restriction key we can quote as Apple-documented is allowActivityContinuation, which governs Handoff. Part 7 covers what that key does and does not inherit.

AirDrop is the share sheet, and nothing else

There is no AirDrop API. What there is is the system share sheet: UIActivityViewController on iOS, NSSharingService and its picker on macOS. Hand the system a value it knows how to represent and AirDrop shows up as one destination among many, for free, with no adoption work at all.

The way apps lose it is by building a fully custom share flow: your own sheet, your own destination list, your own upload. That flow may well be better for your product. It will simply never appear in AirDrop, because AirDrop is not a destination anybody can add to a custom picker. It is something the system sheet offers. If handing a file straight to the machine on the other side of the desk matters to your users, that is an argument for routing at least one path through the system sheet, however good your own is.

What this part cannot tell you

The same discipline the rest of the series runs on applies here, and this section is shorter only because there is less to measure.

  • None of the Continuity Camera claims above are measurements. They are API-shape claims read from Apple's documentation: what the device type is, what the menu item identifier does, what a discovery session returns. We did not instrument a camera pipeline for this series, and you should not read the failure modes as a benchmark.
  • The full MDM Restrictions key table, verbatim from Apple, is not something we have. Apple's live page did not render it in either fetch. allowActivityContinuation is confirmed from Apple's own example payload; every other key named in this series, including allowiPhoneMirroring, is corroborated by MDM vendors and should be treated as vendor-sourced when it matters.
  • We cannot tell you whether iPhone Mirroring will ever gain a detection API. Nothing in Apple's current documentation points to one, and we are not in the business of reading roadmaps. Today's answer is the one above.
  • For Sidecar and Universal Control we found no adoptable API. That is not the same as proving none exists. It is a thorough search that came back empty, which is the strongest claim this kind of research can make.

Which leaves the question that hangs over every part of this series, including the ones with real APIs: how do you actually test any of it, when the feature by definition needs two devices and a human being holding one of them? That is part 9.

Previously (Part 7): Universal Clipboard: The Continuity Feature You Already Shipped

Next up (Part 9): Testing Handoff When You Cannot Automate Itlands in 2 days

Comments

Loading comments…

Adding Continuity to a Mac or iOS app?

We build native SwiftUI and AppKit apps for teams that care about craft, including the Continuity surfaces most apps quietly skip. Book a call and let's look at yours.