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?

Interaction Who acts Can you change it?
Copy (code block, message) SDK, always Behaviour no. Whether the button exists, yes.
Reasoning fold SDK, always Labels and default state, yes.
Task checkbox SDK, always —
Link SDK by default Yes — one line.
Image SDK by default Yes — one line.
Retry, edit You, always The SDK cannot re-run your request.
Custom syntax tap You, 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:

Policy What happens What you hear
.automatic (default) The SDK opens it in-app. didOpenLink / didPresentImage — a notification.
.handledByHost Nothing. linkTapped / imageTapped — act on it or nothing happens.
.disabled Nothing, 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:

  • Requests — linkTapped, imageTapped, extensionTapped. The SDK did nothing. Act on them or nothing happens.
  • Notifications — didOpenLink, 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.