【MarkdownUI】一行代码渲染富文本——SwiftUI 生态最好用的 Markdown 库
iOS三方库精读 · 第 3 期
一、一句话介绍
MarkdownUI 是一个用于 SwiftUI 的 Markdown 渲染库,它让将 Markdown 字符串渲染为完整、美观的富文本视图变得声明式、可主题化,且完全符合 CommonMark 规范。
| 属性 | 信息 |
|---|---|
| ⭐ GitHub Stars | ~2.5k |
| 最新版本 | 2.x(当前 2.3+) |
| License | MIT |
| 支持平台 | iOS 14+ / macOS 11+ / tvOS 14+ / watchOS 7+ |
| Swift 最低版本 | Swift 5.7+ |
| 规范标准 | CommonMark 0.30 |
二、为什么选择它
SwiftUI 原生渲染 Markdown 的痛点
SwiftUI 的 Text 从 iOS 15 开始支持部分 Markdown,但能力极其有限:
原生 Text(markdown:) | MarkdownUI |
|---|---|
| 仅支持行内样式(粗体/斜体/代码) | 完整块级语法(标题/列表/表格/引用) |
| 不支持代码块 | 支持带语言标签的多行代码块 |
| 不支持表格 | 支持 GFM 扩展表格 |
| 不支持引用块(blockquote) | 支持带左侧竖线的引用块 |
| 不支持图片 | 支持远程/本地图片渲染 |
| 无主题系统 | 内置 GitHub / docC / 自定义主题 |
| iOS 15+ 才支持 | iOS 14+ 支持 |
核心优势:
- CommonMark 完整支持,行为与 GitHub/Notion 渲染一致,零惊喜
- 主题系统:
.gitHub/.docC/.basic,一行切换,也可完全自定义 - GFM 扩展:表格、删除线、任务列表等 GitHub Flavored Markdown 扩展
- SwiftUI-native:纯 SwiftUI 渲染,不依赖 WebView,性能好
- 可点击链接,文档类场景开箱即用
三、核心功能速览
基础层(新手必读)
集成方式(SPM 推荐)
在 Xcode → File → Add Package Dependencies 中输入:
https://github.com/gonzalezreal/swift-markdown-ui
或在 Package.swift 中:
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.0.0")
最简单的用法——三行搞定
import SwiftUI
import MarkdownUI
struct ContentView: View {
var body: some View {
Markdown("# Hello **MarkdownUI**\n\n这是一段 *斜体* 文本。")
}
}
切换内置主题
// GitHub 风格(最常用)
Markdown(content).markdownTheme(.gitHub)
// 苹果文档风格
Markdown(content).markdownTheme(.docC)
进阶层(最佳实践)
引用块(Blockquote)——自研渲染器缺失的功能
let quoteContent = """
> ⚠️ **注意**:`atomic` 不等于线程安全。
> 只保证单次读写的原子性,多线程写入仍需加锁。
"""
Markdown(quoteContent).markdownTheme(.gitHub)
// 渲染结果:左侧灰色竖线 + 缩进内容
代码块带语言标签
let codeContent = """
```swift
let value = try await AF
.request(url)
.serializingDecodable(Model.self)
.value
""" Markdown(codeContent).markdownTheme(.gitHub) // 渲染结果:等宽字体 + 灰色背景 + 右上角语言标签
**GFM 表格**
```swift
let tableContent = """
| 修饰符 | 引用计数 | 场景 |
|--------|----------|------|
| `strong` | +1 | 普通对象 |
| `weak` | 不变 | delegate |
| `copy` | +1(副本) | NSString |
"""
Markdown(tableContent).markdownTheme(.gitHub)
自定义主题——局部修改样式
// 只修改代码块背景色,继承 GitHub 其他样式
Markdown(content)
.markdownTheme(
.gitHub.text {
ForegroundColor(.primary)
BackgroundColor(Color(.systemBackground))
}
.codeBlock { label, content in
// 完全自定义代码块视图
VStack(alignment: .leading) {
if let label {
Text(label).font(.caption).foregroundStyle(.secondary)
}
Text(content).font(.system(.body, design: .monospaced))
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(8)
}
)
接收链接点击事件
Markdown(content)
.markdownTheme(.gitHub)
.environment(\.openURL, OpenURLAction { url in
// 自定义处理链接跳转,例如打开应用内浏览器
print("点击链接:\(url)")
return .handled
})
深入层(源码视角)
MarkdownUI 的核心由三层构成:
| 层级 | 职责 | 关键类型 |
|---|---|---|
| Parser | CommonMark 解析,输出 Document AST | Document(基于 cmark-gfm C 库) |
| Theme | 样式声明,定义各 Block/Inline 的渲染规则 | Theme、BlockStyle、InlineStyle |
| Renderer | 将 AST + Theme 转换为 SwiftUI View 树 | MarkdownBody(内部实现) |
解析器底层使用 cmark-gfm(GitHub 官方维护的 CommonMark C 实现),解析速度远超 Swift 正则方案,这也是自研渲染器在复杂文档场景下的性能天花板所在。
四、实战演示
场景:iOS 知识点学习 App,展示带引用、代码、表格的 OC 知识文档页
import SwiftUI
import MarkdownUI
struct OCKnowledgeDetailView: View {
private let content = """
# OC 属性修饰符
## 内存管理修饰符
| 修饰符 | 说明 | 使用场景 |
|--------|------|---------|
| `strong` | 强引用,引用计数 +1 | 普通对象 |
| `weak` | 弱引用,释放后自动置 nil | delegate |
| `copy` | 创建不可变副本 | NSString / Block |
| `assign` | 直接赋值,不改计数 | 基本数据类型 |
## 示例代码
```objc
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, weak) id<PersonDelegate> delegate;
@property (nonatomic, assign) NSInteger age;
@end
```
## 重要注意事项
> ⚠️ **atomic 不等于线程安全**
>
> `atomic` 只保证 setter/getter 的读写原子性,
> 多线程并发写入仍需配合锁或串行队列。
"""
var body: some View {
ScrollView {
Markdown(content)
.markdownTheme(.gitHub)
.padding()
}
.navigationTitle("OC 属性修饰符")
.navigationBarTitleDisplayMode(.large)
.background(Color(.systemGroupedBackground))
}
}
运行效果:表格自动对齐、代码块带 objc 标签、引用块带左侧竖线——这些都是原生 Text 和简单自研渲染器无法覆盖的。
五、源码亮点
进阶层:值得借鉴的用法
EnvironmentValues 驱动的主题系统
MarkdownUI 的主题完全通过 SwiftUI Environment 传递,这意味着你可以在任意层级覆盖样式:
// 全局设置
WindowGroup {
ContentView()
.markdownTheme(.gitHub)
}
// 局部覆盖(子视图会继承,不影响兄弟节点)
VStack {
Markdown(importantContent)
.markdownTheme(.docC) // 只有这一块用 docC 风格
Markdown(normalContent)
// 这里仍然使用外层的 .gitHub
}
深入层:设计思想解析
1. AST + Visitor 模式
解析结果是一棵 Document AST,渲染时用 Visitor 遍历节点,每种节点(Paragraph、Heading、CodeBlock 等)对应 Theme 中的一个渲染闭包。这使得"增加新语法节点"和"修改渲染样式"完全解耦。
2. Protocol-Oriented Theme 定制
Theme 是一个值类型(struct),通过 builder 模式链式修改。每次 .text { ... } 返回新的 Theme 副本,天然线程安全,也符合 SwiftUI 的不可变数据流思想。
六、踩坑记录
问题 1:内容过长时列表页卡顿
问题:在
List中直接嵌套Markdown(...)导致滚动掉帧。
原因:Markdown视图在布局时会解析整个文档,重复计算开销大。
解决:将长内容页面改为独立NavigationLink目标页,或对渲染结果做@State缓存。
// ❌ 不推荐:在 List Cell 中直接渲染长文档
List(items) { item in
Markdown(item.longContent) // 每次滚动都重新解析
}
// ✅ 推荐:只在详情页渲染
List(items) { item in
NavigationLink(item.title) {
ScrollView { Markdown(item.longContent).padding() }
}
}
问题 2:SPM 拉取失败 / 版本冲突
问题:
xcodebuild时报package graph is inconsistent。
原因:Package.resolved中残留了旧版本或冲突条目。
解决:删除*.xcworkspace/xcshareddata/swiftpm/Package.resolved,重新 resolve。
rm HelloWorld.xcworkspace/xcshareddata/swiftpm/Package.resolved
# 重新打开 Xcode,File → Packages → Resolve Package Versions
问题 3:@dynamic 属性在 Markdown 表格中的反引号转义
问题:Markdown 内容含
@dynamic,渲染时反引号内@符号被吞掉。
原因:Swift 字符串多行字面量中,\需要额外转义。
解决:使用原始字符串字面量#"""..."""#或对@做 HTML 实体转义(@)。
// ✅ 使用原始字符串字面量,无需额外转义
private let content = #"""
`@dynamic` 告诉编译器 setter/getter 由运行时动态提供。
"""#
问题 4:深色模式下代码块颜色不对
问题:代码块在 Dark Mode 下背景色与系统不匹配。
原因:.gitHub主题的代码块背景是静态颜色,不随系统变化。
解决:自定义.codeBlock渲染,使用Color(.systemGray6)等语义颜色。
Markdown(content)
.markdownTheme(
.gitHub.codeBlock { _, code in
Text(code)
.font(.system(.footnote, design: .monospaced))
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(.systemGray6)) // 语义颜色,深色模式自动适配
.cornerRadius(8)
}
)
问题 5:引用块嵌套层数过多导致缩进异常
问题:三层及以上嵌套引用(
>>> text)在某些主题下缩进溢出屏幕。
原因:.gitHub主题对嵌套引用没有限制最大缩进。
解决:业务上限制引用嵌套层数,或自定义.blockquote渲染覆盖缩进逻辑。
七、延伸思考
与同类方案的横向对比
| 方案 | 功能完整度 | 依赖 | 定制自由度 | 适用场景 |
|---|---|---|---|---|
| MarkdownUI | ★★★★★ CommonMark 完整 | SPM 外部库 | ★★★★ 主题系统 | 内容丰富型文档页 |
| 自研 MarkdownRenderer | ★★★ 常用子集 | 零依赖 | ★★★★★ 完全自由 | 简单内容/极致定制 |
| SwiftUI Text | ★★ 仅行内样式 | 系统内置 | ★ 几乎不可定制 | 短段落标注 |
| WKWebView + CSS | ★★★★★ | 系统内置 | ★★★★★ | 复杂排版/已有 HTML |
推荐使用 MarkdownUI 的场景
- 文档/帮助中心:内容包含引用块、代码块、链接、图片等完整语法
- 笔记类 App:用户可编辑 Markdown,需要实时预览
- 技术学习 App:知识点页面包含代码示例和对比表格
- 从 Web 迁移:后端返回 Markdown 字符串,需要 iOS 端直接渲染
不推荐使用 MarkdownUI 的场景
- 极度轻量的 SDK:对包体积(+500KB+)极度敏感
- 高度定制的列表 Cell:只需要标题/加粗等基础行内样式,用
Text足够 - 内容格式固定且简单:自研渲染器完全覆盖,无需引入依赖
MarkdownUI vs 自研 MarkdownRenderer 详细对比
本项目中同时维护了两套方案,以下是实测差异:
| 特性 | MarkdownUI | 自研 MarkdownRenderer |
|---|---|---|
引用块 > | ✅ 左侧竖线 + 缩进 | ❌ 原样输出文本 |
| 代码块 | ✅ 等宽 + 语言标签 | ✅ 等宽 + 语言标签 |
| 表格 | ✅ | ✅ |
| 可点击链接 | ✅ | ❌ |
| 图片 | ✅ | ❌ |
| 嵌套列表 | ✅ | ❌ |
| 有序列表编号 | ✅ | ⚠️ 只有 •,忽略序号 |
| 主题 | .gitHub / .docC / 自定义 | 完全自定义 SwiftUI View |
| 包体积 | +500KB+ | 0 |
| iOS 最低版本 | iOS 14+ | iOS 14+(使用了 AttributedString) |
八、参考资源
- GitHub 仓库:gonzalezreal/swift-markdown-ui
- CommonMark 规范:spec.commonmark.org
- cmark-gfm(底层解析器):github/cmark-gfm
- GitHub Flavored Markdown 规范:github.github.com/gfm
- WWDC23 - What's new in SwiftUI:了解 SwiftUI Text 原生 Markdown 的能力边界
- 系列 Demo 仓库:本项目
HelloWorldApp → iOS 热点库学习 → MarkdownUI
九、本期互动
小作业
MarkdownUI 支持完全自定义 .codeBlock 渲染闭包。请尝试实现一个带"复制"按钮的代码块组件:点击按钮将代码内容复制到剪贴板,并短暂显示"已复制 ✓"反馈。
完成标准:
- 代码块右上角有"复制"按钮
- 点击后 1.5 秒内显示"已复制 ✓"提示
- 通过
.markdownTheme的.codeBlock闭包注入,不修改 MarkdownUI 源码
// 启动代码
Markdown(content)
.markdownTheme(
.gitHub.codeBlock { label, code in
// 在这里实现你的带复制按钮的代码块 👇
}
)
思考题
MarkdownUI 底层使用了 C 语言实现的 cmark-gfm 解析器,而本项目的自研 MarkdownRenderer 用 Swift 正则 + 行扫描实现解析。
如果你来设计"下一代自研 Markdown 渲染器",会如何在保持零依赖的同时支持引用块和嵌套列表?你的状态机设计是什么样的?
读者征集
下一期选题征集中——你最想深入了解哪个 iOS 三方库?欢迎评论区留言,候选:Kingfisher、RxSwift、SnapKit、Lottie、GRDB。
你在使用 MarkdownUI 时踩过哪些坑?优质回答会收录进下一期《踩坑记录》。
📅 本系列持续更新 ✅ 第 1 期:Alamofire · ✅ 第 2 期:Kingfisher · ➡️第 3 期:MarkdownUI(本期)