Universal Clipboard: The Continuity Feature You Already Shipped

Every other part of this series is about a feature you choose to adopt. You add an NSUserActivity, you declare a type, you write a receive seam, and until you do none of it happens. This part is the exception. Universal Clipboard is already running in your app, it has been since the day you wrote your first copy button, and the only question that was ever open is whether you said no.
It rides on Handoff, and it is on by default
Universal Clipboard is not a separate transport bolted onto the pasteboard. Apple Platform Security, in its Handoff security section, puts it in one sentence:
Universal Clipboard leverages Handoff to securely transfer the content of a user's clipboard across devices.
Apple Platform Security, Handoff security
The same page states the posture that makes this part necessary: clipboard content "is shared by default with Universal Clipboard unless the app developer chooses to disallow sharing." Read that as an API contract rather than a feature description. The decision is per item, it belongs to the app doing the writing, and the default is share.
So the defect is an assignment, on a line nobody would stop at in review:
// iOS: syncs to every device signed into the
// same Apple Account.
UIPasteboard.general.string = record.taxID
// macOS: same, and there is no flag below
// that stops it.
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(
record.taxID, forType: .string
)Nothing in either API asks a question. There is no content-based filtering anywhere in the system: nothing inspects that string, notices it is a customer's tax number rather than the user's own shopping list, and holds it back. The platform sees a string on the general pasteboard and does exactly what the user asked it to do when they turned Handoff on.
The distinction that matters for the rest of this article is whose data is on screen. A note-taking app copying the user's own paragraph to the user's own iPad is Universal Clipboard working as designed, and it is genuinely delightful. An app that displays other people's confidential records (a CRM, a practice-management tool, anything B2B) is making a different transfer with the same one-liner.
The iOS opt-out: localOnly
On iOS there is a real answer. UIPasteboard.OptionsKey.localOnly, available since iOS 10.0, is documented as "A Boolean value that specifies that the pasteboard items should not be available to other devices through the Handoff feature." It goes in the options dictionary of setItems(_:options:), alongside an optional expirationDate that removes the items after a date.
import UIKit
import UniformTypeIdentifiers
func copyLocally(_ value: String) {
let type = UTType.utf8PlainText.identifier
let expiry = Date()
.addingTimeInterval(120)
UIPasteboard.general.setItems(
[[type: value]],
options: [
.localOnly: true,
.expirationDate: expiry
]
)
}Since iOS 11 the same two controls exist as typed overloads, which is usually the nicer call site: setItemProviders(_:localOnly:expirationDate:) and setObjects(_:localOnly:expirationDate:). Note the bridge cast: the generic constraint is NSItemProviderWriting, which a Swift String does not satisfy on its own.
UIPasteboard.general.setObjects(
[value as NSString],
localOnly: true,
expirationDate: expiry
)One detail in that API bites, and it is worth saying out loud because it is easy to walk into while doing the right thing. The options apply to every item placed in that call. If you batch a shareable link and a confidential identifier into one setItems call, you get one options dictionary for both: either the link is needlessly pinned to the device, or the identifier syncs. Two writes with two postures cannot share a call. Split them, or decide the whole call is local.
On macOS there is no opt-out at all
This is the sentence the part exists for. Apple's own NSPasteboard reference says of the general pasteboard that it
automatically participates with the Universal Clipboard feature in macOS 10.12 and later and in iOS 10.0 and later. There is no macOS API for interacting with this feature.
Apple, NSPasteboard reference
That leaves exactly two mitigations, both architectural rather than API-level. The first is to not put the value on a pasteboard at all. Rethinking the affordance is often the correct answer, and it is the subject of a section below. The second is to use a named pasteboard when the content is not meant for the user's own cross-app paste in the first place.
import AppKit
extension NSPasteboard.Name {
static let internalTransfer =
NSPasteboard.Name(
"com.example.app.transfer"
)
}
let board = NSPasteboard(
name: .internalTransfer
)
board.clearContents()
board.setString(value, forType: .string)Be honest about what that buys. A named pasteboard is not a security boundary and not an encrypted store; it is a different pasteboard, one the system's Universal Clipboard participation does not cover and one that Command-V will never read from. That makes it right for an internal transfer (a drag payload, a move between two of your own windows, a value your app will read back itself) and wrong for anything the user expects to paste into Mail. If the user needs to paste it into Mail, the general pasteboard is the only route, and on macOS that route crosses devices. There is no third option to design around.
The community markers are not an opt-out
Search for a macOS workaround and you will find nspasteboard.org, an unofficial convention that defines org.nspasteboard.ConcealedType, org.nspasteboard.TransientType and org.nspasteboard.AutoGeneratedType. Password managers and clipboard managers honour them, and the convention is a genuinely good citizen of the Mac ecosystem.
It is also not the thing you are looking for, and reaching for it as a fix is a mistake worth naming. These types are not recognised by AppKit. They are honoured voluntarily, by the apps that choose to read them. And we found no evidence that any of them suppresses Universal Clipboard sync: they address a different problem, which is third-party clipboard history recording your value locally. Marking a string ConcealedType and calling the macOS exposure handled is a fix that exists only in the changelog.
What the audit found
We went looking in one production SwiftUI/AppKit app we build. It is deliberately not named, as everywhere in this series. Pasteboard writes live in six Swift files on the iOS side and fifteen on the macOS side, the macOS count including UI tests. Eight copy affordances place customer data on the general pasteboard with no options set, and therefore on every device signed into that person's Apple Account.
Not one of those eight is a bug in the ordinary sense. Each was written by a competent engineer adding a convenience a user asked for, and none of them had heard of Universal Clipboard as a developer-facing contract. Nothing in the API makes you think about it. UIPasteboard.general.string is an assignment. It has no completion handler, no options argument in that form, no warning, no documentation note at the call site. The cross-device behaviour is a property of the system, not of the line you wrote, and the line you wrote is the shortest one in the file.
The remediation splits along the platform line described above, and that split is the honest shape of the finding: the iOS affordances have a fix, and the macOS ones do not. For those, the decision is product-level: keep the affordance and accept that the value reaches the user's other devices, or remove it. By Apple's own sentence there is no third answer to choose.
A helper that makes the safe path the default
Once a codebase has more than a couple of copy sites, the only durable fix is to stop calling the pasteboard APIs directly. A small helper does two useful things: it makes the restrictive posture what you get when you forget to think, and it gives macOS somewhere to say that the request could not be honoured.
#if canImport(UIKit)
import UIKit
#else
import AppKit
#endif
enum Clipboard {
enum Reach {
/// The user's own content. May travel
/// via Universal Clipboard.
case anyDevice
/// Someone else's content. iOS honours
/// this; macOS cannot.
case thisDeviceOnly
}
/// Returns false when `reach` could not
/// be honoured: `.thisDeviceOnly` on
/// macOS, where the value will sync.
@discardableResult
static func copy(
_ text: String,
reach: Reach = .thisDeviceOnly
) -> Bool {
#if canImport(UIKit)
UIPasteboard.general.setObjects(
[text as NSString],
localOnly: reach == .thisDeviceOnly,
expirationDate: nil
)
return true
#else
let board = NSPasteboard.general
board.clearContents()
board.setString(
text, forType: .string
)
return reach == .anyDevice
#endif
}
}The default argument is the whole point: a call site that has not thought about Continuity gets the local behaviour on iOS instead of the syncing one. The return value is the second point. It is not decoration. It is the only place in the codebase where the macOS gap becomes visible to a caller who can do something about it, whether that is logging the exposure, showing the user what will happen, or refusing to offer the affordance on that platform at all.
A Copy button on someone else's data is a product decision
The reflex to put a small copy icon on every row is strong, and on a consumer app it costs nothing. On an app holding other people's records it is a decision about where that data is allowed to go, taken by whoever added the icon, usually without the meeting. Offer the affordance where a user genuinely needs to paste the value somewhere else: into an email, a form, a support ticket. Do not add one to a row because the row looked bare.
Two rules we now apply without argument:
- Never copy a credential, token, session identifier or API key to a general pasteboard on either platform. On iOS that write needs localOnly at minimum; on macOS there is no way to stop it leaving the device, so the answer is a different affordance: reveal it, let the user select it, hand it to the Keychain, anything but NSPasteboard.general.
- Decide the posture at the call site, not in the review. Every copy site in a data-holding app answers one question: is this the user's own content, or someone else's? The first may cross devices. The second may cross devices only if a person decided it should.
Reading is a user-visible act on iOS
The write side is invisible; the read side is the opposite, and a lot of apps discovered this the hard way. Since iOS 16, a programmatic read of content another app placed on the pasteboard raises a system prompt that reads "[App] would like to paste from [Source App]". Since iOS 14, a non-interactive read raises a user-visible banner. That prompt is suppressed when the paste is user-chosen: the system Paste menu item, Command-V on a hardware keyboard, or the sanctioned UIPasteControl.
If all you need is the shape of what is on the pasteboard (should this button be enabled? is there a URL here?) you can ask without consuming it and without notifying anyone:
let board = UIPasteboard.general
guard board.hasURLs || board.hasStrings else {
return
}
let found = try await board.detectedPatterns(
for: [.probableWebURL]
)
let looksLikeLink =
found.contains(.probableWebURL)The design lesson generalises past the prompt. An app that reads the pasteboard on every foreground, to be helpful, is an app that has chosen to interrupt its users with a permission dialog they did not ask for. Read on an explicit action, or do not read.
Two things you cannot build
The first is provenance. We went looking for an API on either platform that tells a reading app which device wrote the content it is about to read. There is none. That is a searched-for negative finding, not an unexplored corner, and it closes off a whole category of design: any rule of the form "treat a paste that arrived from another device differently" is not implementable today. You cannot detect the case, so you cannot handle it.
The second is an MDM boundary, and this one is more often assumed than checked. Managed Pasteboard (requireManagedPasteboard, and the allowOpenFromManagedToUnmanaged / allowOpenFromUnmanagedToManaged pair) governs same-device copy and paste between managed and unmanaged apps. It is not a Universal Clipboard control, and it is not the same axis. It is also not airtight: third parties have reported unpatched bypasses, via the Share sheet on highlighted text and via Shortcuts laundering the provenance of a value. Those reports are not Apple-acknowledged, and we are repeating them as reports rather than as verified behaviour. Either way, describe Managed Pasteboard to a customer as a control, never as a guarantee.
The nearest thing to a real lever is allowActivityContinuation, the restriction that turns Handoff off, which appears verbatim in Apple's own example Restrictions payload. Because Universal Clipboard rides on Handoff, turning Handoff off should take the clipboard with it. Note two things before you rely on that. The inheritance is our inference from the architecture, not something Apple states. And the switch belongs to the customer's administrator rather than to you.
What we could not find
No Apple guidance on pasteboard content sensitivity for third-party apps. We looked for a Human Interface Guidelines page and an App Review note covering what an app should and should not place on the general pasteboard, and in particular anything addressing an app that handles other people's confidential data. Neither exists as far as we could establish.
That absence is the reason a decision that moves customer data between devices gets made in a pull request that adds a small copy icon to a table row. There is no rule to cite in review, so the argument has to be made from the platform behaviour instead. That is what this article is for. If Apple has published such guidance and we missed it, we would genuinely like to be corrected.
Part 8 stays with the same theme from the other end: the Continuity features that expose no developer API at all, where the absence of a call to make is itself the design constraint.
Previously (Part 6): What Must Never Cross a Handoff
Next up (Part 8): Continuity Camera and the Features With No APIlands tomorrow
Comments
Loading comments…