Swift Markdown Kit · DOCS

单文档渲染

单文档渲染

适用于「一篇内容」而不是「一段会话」的场景:文章、更新日志、帮助页、单条模型 回答。

SwiftUI

import SwiftUI
import SwiftMarkdownKit

struct ArticleView: View {
    let article: String

    var body: some View {
        MarkdownRenderView(markdown: article)
    }
}

绑定一个不断变长的字符串就能实现流式:新值是旧值的前缀扩展时,视图只追加增量; 其他情况才整体重渲染。

想观察渲染器的状态,传一个回调:

MarkdownRenderView(markdown: article) { event in
    switch event {
    case .ready:
        break
    case .rendered(let height), .heightChanged(let height):
        print("height", height)
    case .action(let action):
        print("action", action)
    case .error(let error):
        print(error.code, error.message)
    @unknown default:
        break
    }
}

UIKit

import UIKit
import SwiftMarkdownKit

final class ArticleViewController: UIViewController {
    private let renderer = MarkdownRenderViewController()

    override func viewDidLoad() {
        super.viewDidLoad()

        addChild(renderer)
        renderer.view.frame = view.bounds
        renderer.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(renderer.view)
        renderer.didMove(toParent: self)

        renderer.render("# Hello\n\n它 **能用**。")
    }
}

render(_:) 替换内容,clear() 清空,reload() 从头重建渲染器 —— 最后一个 通常用不到:WebKit 在内存压力下回收内容进程时,SDK 会自动重新加载并回放内容。

单文档的流式

renderer.startStreaming()
renderer.appendChunk("流式 **Markdown")
renderer.appendChunk("**,带公式 $x^2$", isFinal: true)

或者每次把累计的全文交给它,由它算增量:

renderer.renderStream(everythingSoFar)

两条路径都走增量渲染:已完成块边界之前的 HTML 会被缓存,只有不稳定的尾部重新 渲染,所以整条流的总成本随最终长度线性增长,而不是平方增长。

布局

渲染器默认自己滚动。嵌进你自己的 ScrollView 时,关掉它并改用高度回调:

struct EmbeddedArticle: View {
    let markdown: String
    @State private var height: CGFloat = 0

    var body: some View {
        ScrollView {
            VStack {
                Text("上面的任意内容")

                MarkdownRenderView(
                    markdown: markdown,
                    options: MarkdownRenderOptions(isScrollEnabled: false)
                ) { event in
                    if case .rendered(let measured) = event { height = measured }
                    if case .heightChanged(let measured) = event { height = measured }
                }
                .frame(height: height)
            }
        }
    }
}

流式过程中 heightChanged 会防抖,避免每帧触发一次布局;间隔由 heightChangeDebounceInterval 控制。

解析器的容错

模型输出不是教科书式的 Markdown,解析器就是为此设计的。价格旁边的 $、方括号 引用、右花括号还没流完的公式,都不会变红。KaTeX 解析不了的内容会退回模型实际 写下的字符,颜色与正文一致。

var options = MarkdownRenderOptions()
options.markdown.math = true          // KaTeX,行内与块级
options.markdown.taskLists = true     // - [x] / - [ ]
options.markdown.emoji = true         // :smile:
options.markdown.linkify = true       // 裸 URL 自动成为链接
options.markdown.typographer = true   // 智能引号、破折号、省略号
options.markdown.breaks = false       // 单个换行转成 <br>
options.markdown.allowRawHTML = false // 模型输出请保持关闭

allowRawHTML 默认关闭,对模型写的内容应当一直保持关闭。推理折叠不需要它 —— <think> 的识别不依赖开放原始 HTML。

var options = MarkdownRenderOptions()
options.markdown.reasoning.completedLabel = "已深度思考"

这里配置的是单文档里的折叠。会话请改用 MarkdownChatRenderOptions.reasoning,它同时还驱动模型思考过程中的实时折叠。