Swift Markdown Kit · DOCS

Interaction

Interaction

What happens when the reader taps a link, an image, a copy button or a retry button — and how to take any of it over.

The rule

The SDK draws one line: does the action stay inside the rendered content?

InteractionWho actsCan you change it?
Copy (code block, message)SDK, alwaysBehaviour no. Whether the button exists, yes.
Reasoning foldSDK, alwaysLabels and default state, yes.
Task checkboxSDK, always
LinkSDK by defaultYes — one line.
ImageSDK by defaultYes — one line.
Retry, editYou, alwaysThe SDK cannot re-run your request.
Custom syntax tapYou, always

Copying and folding need nothing from your app, so delegating them would only be work for you. Opening a link or a picture leaves the rendered surface, and there your app may legitimately want the last word.

Links and images

Do nothing and both work: links open in an in-app SFSafariViewController, images open in a full-screen viewer with pinch zoom, double-tap and drag-to- dismiss.

To take one back:

var options = MarkdownChatRenderOptions()
options.interaction.images = .handledByHost   // links stay automatic
MarkdownChatRenderView(messages: messages, options: options) { event in
    guard case .action(let event) = event,
          case .imageTapped(let url) = event.action, let url else { return }
    presentMyOwnViewer(url)
}

Three policies, per interaction:

PolicyWhat happensWhat you hear
.automatic (default)The SDK opens it in-app.didOpenLink / didPresentImage — a notification.
.handledByHostNothing.linkTapped / imageTapped — act on it or nothing happens.
.disabledNothing, and the element stops looking tappable.Nothing.

options.interaction = .manual sets both to .handledByHost at once.

Notifications are not vetoes

The two families of event are deliberately distinct:

  • RequestslinkTapped, imageTapped, extensionTapped. The SDK did nothing. Act on them or nothing happens.
  • NotificationsdidOpenLink, didPresentImage, codeCopied, reasoningToggled. The SDK already did it. Acting again opens the same thing twice.

There is no callback that can cancel an automatic action. To take control, change the policy — that is what it is for.

Link safety

Rendered text is model output, and a model can be talked into emitting javascript:, file://, or a deep link into your own app. A scheme outside the allowlist is therefore never opened and never handed to you:

options.interaction.allowedLinkSchemes   // ["http", "https", "mailto"] by default
options.interaction.allowedLinkSchemes.insert("myapp")

Anything else arrives as linkBlocked(url:scheme:) so you can log it. This applies under every policy, including .handledByHost — an app that took over link handling is still not a reason to hand it javascript:.

Both renderers also refuse any navigation that would replace the rendered page. Link taps are cancelled inside the page and routed through the bridge, so a navigation reaching that layer is unexpected by definition.

Copy

Copying is never delegated: the SDK writes to the pasteboard and tells you what happened. What you can change is whether the affordance exists at all, which some apps must be able to remove for data-loss-prevention reasons:

options.interaction.allowsCodeCopy = false          // no copy button on code blocks
options.messageActions.assistantActions = []        // no toolbar under messages
MarkdownChatRenderView(messages: messages) { event in
    guard case .action(let event) = event else { return }
    if case .codeCopied = event.action { showToast("Copied") }
    if case .codeCopyFailed(_, _, let error) = event.action { showToast(error.message) }
}

Retry and edit

The SDK cannot re-run a request or write into a composer it does not own, so these buttons appear only when you list them — and listing them is your promise to handle messageAction. See Streaming & Chat.

Custom syntax

A tap on a registered inline extension — an @mention, a [[wikilink]] — arrives as extensionTapped(name:action:value:) and is always yours to route. See Theming & Syntax.