English | δΈζ
A powerful iOS Markdown rendering component built on TextKit 2, providing smooth rendering performance and rich customization options. It also enables the streaming rendering of Markdown format in AI question-and-answer scenarios.
π MarkdownDisplayView delivers streaming rendering effects comparable to leading AI terminal iOS clients like ChatGPT, Claude, Doubao, DeepSeek, and Grok, while offering even richer customization features and configuration options.
- Effects Showcase
- Demo Effects
- Features
- Requirements
- Installation
- Quick Start
- Custom Configuration
- Table of Contents
- Supported Markdown Syntax
- Complete Example
- Performance Optimization
- Advanced Usage
- Custom Extensions
- Troubleshooting
- Changelog
- Contributing
- License
- Author
- Acknowledgments
- Contact
- Simulated streaming
- Chat with AI model
Config.local.json structure:
- π High-Performance Rendering β Based on TextKit 2, supports asynchronous rendering, incremental updates, streaming rendering, etc. Instant loading with ultra-fast first screen rendering.
- β‘ Low CPU Usage β Streaming mode supports nested style rendering with CPU peak < 56% on iPhone 17 Pro simulator, averaging only 30%.
- π¨ Full Markdown Support β Formula of LaTeX protocol, Headings, lists, tables, code blocks (with horizontal scrolling), blockquotes, images, and more.
- π Syntax Highlighting β Supports syntax highlighting for 20+ programming languages (Swift, Python, JavaScript, etc.).
- π Automatic Table of Contents β Automatically extracts headings to generate an interactive TOC.
- π― Highly Customizable β Comprehensive configuration for fonts, colors, spacing, etc.
- π Custom Extensions β Support for custom inline syntax parsing and code block renderers (e.g., Mermaid diagrams).
- π Event Callbacks β Link taps, image taps, TOC navigation.
- π± Native iOS β Built with UIKit and TextKit 2 for excellent performance.
- π Dark Mode β Built-in light and dark theme configurations.
- π³ Haptic Feedback β Supports synchronized haptic feedback during streaming output for enhanced interaction experience.
- iOS 15.0+ (due to TextKit 2 requirement)
- Swift 5.9+
- Xcode 16.0+
- Open your project in Xcode.
- Choose
FileβAdd Package Dependencies... - Enter the repository URL:
https://github.com/zjc19891106/MarkdownDisplayView.git - Select the version and click
Add Package.
Add the dependency in Package.swift:
dependencies: [
.package(url: "https://github.com/zjc19891106/MarkdownDisplayView.git", from: "1.8.9")
]Package.swift resolves Kingfisher from 8.9.0 automatically for asynchronous image loading and caching.
Add the following lines to your Podfile:
pod 'MarkdownDisplayKit', '~> 1.8.9'Then run:
pod installNote: MarkdownDisplayKit.podspec declares AppleSwiftMDWrapper for Markdown parsing and Kingfisher (~> 8.9.0) for image loading, so CocoaPods resolves those dependencies during pod install.
import UIKit
import MarkdownDisplayView
class ViewController: UIViewController {
private let markdownView = ScrollableMarkdownViewTextKit()
override func viewDidLoad() {
super.viewDidLoad()
// Add to view hierarchy
view.addSubview(markdownView)
markdownView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
markdownView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
markdownView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
markdownView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
markdownView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
// Set Markdown content
markdownView.markdown = """
# Welcome to MarkdownDisplayView
This is a **powerful** Markdown rendering component.
## Key Features
- Full Markdown syntax support
- Code syntax highlighting
- Automatic table of contents generation
- Asynchronous image loading
### Code Example
```swift
let message = "Hello, World!"
print(message)
```
[Visit GitHub](https://github.com)
"""
}
}markdownView.onLinkTap = { url in
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}markdownView.onImageTap = { imageURL in
print("Image tapped: \(imageURL)")
// You can implement image preview functionality here
}// Use default light theme
markdownView.configuration = .default
// Use dark theme
markdownView.configuration = .darkvar config = MarkdownConfiguration.default
// Custom fonts
config.bodyFont = .systemFont(ofSize: 17)
config.h1Font = .systemFont(ofSize: 32, weight: .bold)
config.codeFont = .monospacedSystemFont(ofSize: 15, weight: .regular)
// Custom colors
config.textColor = .label
config.linkColor = .systemBlue
config.linkUnderlineEnabled = false // Disable link underline
config.codeBackgroundColor = .systemGray6
config.blockquoteTextColor = .secondaryLabel
// Custom spacing
config.paragraphSpacing = 16
config.headingSpacing = 20
config.imageMaxHeight = 500
config.lineSpacing = MarkdownLineSpacingConfiguration(
body: 6,
heading: 8,
quote: 6,
codeBlock: 4
)
// Apply configuration
markdownView.configuration = configpublic var bodyFont: UIFont // Body font
public var h1Font: UIFont // H1 heading font
public var h2Font: UIFont // H2 heading font
public var h3Font: UIFont // H3 heading font
public var h4Font: UIFont // H4 heading font
public var h5Font: UIFont // H5 heading font
public var h6Font: UIFont // H6 heading font
public var codeFont: UIFont // Code font
public var blockquoteFont: UIFont // Blockquote fontpublic var textColor: UIColor // Text color
public var headingColor: UIColor // Heading color
public var linkColor: UIColor // Link color
public var linkUnderlineEnabled: Bool // Whether links display underline (default: true)
public var codeTextColor: UIColor // Code text color
public var codeBackgroundColor: UIColor // Code background color
public var blockquoteTextColor: UIColor // Blockquote text color
public var blockquoteBarColor: UIColor // Blockquote border color
public var tableBorderColor: UIColor // Table border color
public var tableHeaderBackgroundColor: UIColor // Table header background
public var tableRowBackgroundColor: UIColor // Table row background
public var tableAlternateRowBackgroundColor: UIColor // Table alternate row background
public var horizontalRuleColor: UIColor // Horizontal rule color
public var imagePlaceholderColor: UIColor // Image placeholder color
public var footnoteColor: UIColor // Footnote color
public var tocTextColor: UIColor // TOC text color
public var detailsSummaryTextColor: UIColor // Details summary text colorpublic var paragraphSpacing: CGFloat // Paragraph spacing
public var headingSpacing: CGFloat // Heading spacing
public var listIndent: CGFloat // List indentation
public var codeBlockPadding: CGFloat // Code block padding
public var blockquoteIndent: CGFloat // Blockquote indentation
public var imageMaxHeight: CGFloat // Maximum image height
public var imagePlaceholderHeight: CGFloat // Image placeholder heightpublic var lineSpacing: MarkdownLineSpacingConfiguration // Role-based line spacing config
public struct MarkdownLineSpacingConfiguration {
public var body: CGFloat
public var heading: CGFloat
public var quote: CGFloat
public var codeBlock: CGFloat
}public var latexFontSize: CGFloat // LaTeX formula font size (default: 22)
public var latexAlignment: NSTextAlignment // LaTeX formula alignment (.left, .center, .right)
public var latexBackgroundColor: UIColor // LaTeX formula background color
public var latexPadding: CGFloat // LaTeX formula padding (default: 20)public var blockquoteBackgroundColor: UIColor // Blockquote background color
public var blockquoteBarWidth: CGFloat // Blockquote left bar width (default: 4)
public var blockquoteContentSpacing: CGFloat // Blockquote content spacing (default: 8)
public var blockquoteContentPadding: CGFloat // Blockquote content padding (default: 12)public var tableMinColumnWidth: CGFloat // Table minimum column width (default: 80)
public var tableMaxColumnWidth: CGFloat // Table maximum column width (default: 200)
public var tableRowHeight: CGFloat // Table row height (default: 44)
public var tableCellPadding: CGFloat // Table cell padding (default: 16)
public var tableSeparatorHeight: CGFloat // Table separator height (default: 1)
public var autoFixMalformedTables: Bool // Auto-fix malformed table text from streaming/LLM output (default: true)public var listItemSpacing: CGFloat // List item spacing (default: 4)
public var listMarkerMinWidth: CGFloat // List marker minimum width (default: 20)
public var listMarkerSpacing: CGFloat // List marker to content spacing (default: 4)
public var listTopPadding: CGFloat // Whole-list top padding (default: 0)
public var listBottomPadding: CGFloat // Whole-list bottom padding (default: 0)public var detailsSummaryFont: UIFont // Details summary font
public var detailsSummaryTextColor: UIColor // Details summary text color
public var detailsSummaryMinHeight: CGFloat // Details summary minimum height (default: 40)
public var detailsContentPadding: CGFloat // Details content padding (default: 12)
public var detailsSpacing: CGFloat // Details internal spacing (default: 8)public var syntaxColors: SyntaxHighlightColors // Syntax highlighting colors (light theme)
public var syntaxColorsDark: SyntaxHighlightColors // Syntax highlighting colors (dark theme)
// SyntaxHighlightColors structure
public struct SyntaxHighlightColors {
public var keyword: UIColor // Keyword color
public var string: UIColor // String color
public var number: UIColor // Number color
public var comment: UIColor // Comment color
public var type: UIColor // Type color
public var function: UIColor // Function color
public var property: UIColor // Property color
public var preprocessor: UIColor // Preprocessor color
public static var xcode: SyntaxHighlightColors // Xcode light theme
public static var xcodeDark: SyntaxHighlightColors // Xcode dark theme
}public var streamingHapticFeedbackStyle: StreamingHapticFeedbackStyle // Haptic feedback style (default: .none)
public var streamingHapticMinInterval: TimeInterval // Minimum interval between haptics (default: 0.05s)
// StreamingHapticFeedbackStyle enum
public enum StreamingHapticFeedbackStyle {
case none // No haptic feedback (default)
case light // Light haptic feedback
case medium // Medium haptic feedback
case heavy // Heavy haptic feedback
case soft // Soft haptic feedback (iOS 13+)
case rigid // Rigid haptic feedback (iOS 13+)
}
// Usage example
var config = MarkdownConfiguration.default
config.streamingHapticFeedbackStyle = .light // Enable light haptic feedback
config.streamingHapticMinInterval = 0.05 // 50ms minimum interval
markdownView.configuration = config// Markdown content automatically parses headings to generate TOC
let tocItems = markdownView.tableOfContents
for item in tocItems {
print("Level \(item.level): \(item.title)")
}// Automatically generate clickable TOC view
let tocView = markdownView.generateTOCView()
// Add to interface
view.addSubview(tocView)// Scroll to corresponding position when TOC item is tapped
markdownView.onTOCItemTap = { item in
markdownView.scrollToTOCItem(item)
}# H1 Heading
## H2 Heading
### H3 Heading
#### H4 Heading
##### H5 Heading
###### H6 Heading**Bold text**
*Italic text*
***Bold and italic***
~~Strikethrough~~
`Inline code`- Item 1
- Item 2
- Nested item 2.1
- Nested item 2.21. First item
2. Second item
1. Nested 2.1
2. Nested 2.2- [x] Completed task
- [ ] Pending task[Link text](https://example.com)
> This is a blockquote
> Can contain multiple lines
>> Nested blockquotes are supportedSupported programming languages for syntax highlighting:
- Swift, Objective-C
- JavaScript, TypeScript, Python, Ruby
- Java, Kotlin, Go, Rust
- C, C++, Shell, SQL
- HTML, CSS, JSON, YAML
- And more...
```swift
func greet(name: String) -> String {
return "Hello, \(name)!"
}
print(greet(name: "World"))
```| Column1 | Column2 | Column3 |
|---------|---------|---------|
| A1 | B1 | C1 |
| A2 | B2 | C2 |---
***
___<details>
<summary>Click to expand</summary>
This is the collapsed content
Can contain any Markdown syntax
</details>This is text with a footnote[^1]
[^1]: This is the footnote contentCheck out the complete example project in the Example/ExampleForMarkdown directory, which includes:
- All Markdown syntax rendering effects
- Custom configuration examples
- Video, Mermaid, and ECharts custom extension examples
- Event callback handling
- Performance testing
For CocoaPods integration, see the CocoapodsMDExample project, which contains the same custom extensions.
Run the example project:
cd Example/ExampleForMarkdown
open ExampleForMarkdown.xcodeproj- Asynchronous Rendering - Markdown parsing and rendering execute in background queue, not blocking the main thread
- Incremental Updates - Uses Diff algorithm, only updates changed parts
- Lazy Image Loading - Images load asynchronously through Kingfisher with cache reuse
- Regex Caching - Syntax highlighting regex expressions are cached and reused
- View Reuse - Efficient view update strategy
let markdownView = MarkdownViewTextKit()
// You need to manage the scroll container yourselfPersist the original Markdown as the source of truth. For stable, non-streaming messages, prepare the rendered content on a background queue and keep it in an in-memory cache keyed by message identity, Markdown content, container width, and style version:
let renderer = MarkdownRenderer(
configuration: configuration,
containerWidth: markdownWidth
)
let prepared = renderer.prepare(message.markdown)
DispatchQueue.main.async {
markdownView.setPreparedContent(prepared)
}setPreparedContent(_:) skips Markdown parsing, render-element creation, and the first height-estimation pass. Rebuild the prepared content after the Markdown, width, or configuration changes. MarkdownPreparedContent is intended as an in-memory render cache; store the original Markdown rather than archiving it as the durable history format.
For tens or hundreds of long documents, do not prepare the complete history eagerly. Use UITableViewDataSourcePrefetching to prepare only rows near the visible range, cancel obsolete work after fast scrolling or width changes, and use NSCache with both countLimit and totalCostLimit so prepared attributed strings can be evicted under memory pressure.
The example controllers use the normal render path for short Markdown and show a lightweight loading indicator while a cache-missed long document is prepared once, avoiding simultaneous normal parsing and cache preparation for the same content.
See AIChatViewController.swift and HistoryMDViewController.swift for visible-range prefetching, bounded prepared-content caches, and cached table-row estimates.
let markdownView = MarkdownViewTextKit()
markdownView.onHeightChange = { newHeight in
print("Content height changed to: \(newHeight)")
// Can be used to dynamically adjust container height
}
// Set link tap callback
markdownView.onLinkTap = { [weak self] url in
// Handle link tap
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
markdownView.onImageTap = { imageURL in
// Built-in ImageView loads and caches remote images through Kingfisher.
// Use imageURL to present your own preview if needed.
}
markdownView.onTOCItemTap = { item in
print("title:\(item.title), level:\(item.level), id:\(item.id)")
}let scrollableView = ScrollableMarkdownViewTextKit()
view.addSubview(scrollableMarkdownView)
scrollableMarkdownView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollableMarkdownView.topAnchor.constraint(
equalTo: view.topAnchor, constant: 88),
scrollableMarkdownView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollableMarkdownView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollableMarkdownView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
// Built-in UIScrollView, automatically handles scrolling
scrollableMarkdownView.onLinkTap = { [weak self] url in
// Handle link tap
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
scrollableMarkdownView.onImageTap = { imageURL in
// Built-in ImageView loads and caches remote images through Kingfisher.
// Use imageURL to present your own preview if needed.
}
scrollableMarkdownView.onTOCItemTap = { item in
print("title:\(item.title), level:\(item.level), id:\(item.id)")
}
scrollableMarkdownView.markdown = sampleMarkdown
// Back to table of contents
scrollableMarkdownView.backToTableOfContentsSection()- Other aspects are consistent with the scrollable markdown view above
// Difference is in displaying content
private func loadSampleMarkdown() {
// Streaming render (typewriter effect)
scrollableMarkdownView.startStreaming(
sampleMarkdown,
unit: .word,
unitsPerChunk: 2,
interval: 0.1,
)
}
// If you need to show all content immediately (e.g., user clicks skip)
@objc private func skipButtonTapped() {
scrollableMarkdownView.markdownView.finishStreaming()
}For real-time streaming from LLM APIs (like ChatGPT, Claude) where content arrives in chunks:
class ChatViewController: UIViewController {
private let scrollableMarkdownView = ScrollableMarkdownViewTextKit()
// Start real streaming mode
func startLLMStream() {
scrollableMarkdownView.markdownView.beginRealStreaming()
}
// Append chunks as they arrive from the API
func onChunkReceived(_ chunk: String) {
scrollableMarkdownView.markdownView.appendStreamData(chunk)
}
// Call when stream completes
func onStreamComplete() {
scrollableMarkdownView.markdownView.endRealStreaming()
}
}Recommended configuration for streaming AI chat in table/collection cells:
var config = MarkdownConfiguration.default
config.typewriterTextMode = .append
config.typewriterHeightUpdateInterval = 20
config.streamMinModuleLength = 20
scrollableMarkdownView.markdownView.configuration = configKey Features:
- Smart Buffering: Automatically buffers incomplete Markdown structures (unclosed code blocks, tables, LaTeX)
isPlainText()Detection:MarkdownStreamBufferdetects non-Markdown content- Faster Plain Text Streaming: For plain text without Markdown markers, module submission can happen at
\nboundaries instead of strictly waiting for\n\n - Markdown Behavior Unchanged: Markdown content still waits for
\n\nparagraph boundaries - Incremental Rendering: Renders complete modules immediately while buffering incomplete content
- Typewriter Effect: Smooth character-by-character animation for rendered content
MarkdownDisplayKit supports custom extensions to add your own Markdown syntax and rendering.
The custom extension implementations live in the example projects and are not registered automatically by the MarkdownDisplayView library target. Both demos contain the same implementations:
- Swift Package example:
Example/ExampleForMarkdown/ExampleForMarkdown - CocoaPods example:
CocoapodsMDExample/CocoapodsMDExample - Registration entry point:
AppDelegate.swift - Complete Markdown usage: the βCustom Style Testsβ section in
MarkdownExampleViewController.swift
| Example | Extension mechanism | Syntax | Current capabilities |
|---|---|---|---|
| Video | MarkdownCustomParser + MarkdownCustomViewProvider + MarkdownCustomActionHandler |
[video:filename] |
Thumbnail, duration, QuickLook playback; supports .mov, .mp4, and .m4v |
| Mermaid | MarkdownCodeBlockRenderer |
```mermaid |
Flowcharts, sequence diagrams, class diagrams, state diagrams, Gantt charts, and mind maps |
| ECharts | MarkdownCustomParser + MarkdownCustomViewProvider |
<echarts height="320">JSON</echarts> |
Bar, pie, line, scatter, stacked area, candlestick, histogram, graph, and heatmap examples |
Source files:
Register the video extension in AppDelegate:
import MarkdownDisplayView
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Register video extension
MarkdownCustomExtensionManager.shared.registerVideoExtension()
return true
}Syntax: [video:filename]
## Video Demo
[video:myVideo]
Supported formats: .mov, .mp4, .m4vFeatures:
- Auto-generates video thumbnail
- Displays video duration
- Click to play with QuickLook
Implement three protocols to create your own extension:
class MentionParser: MarkdownCustomParser {
let identifier = "mention"
let pattern = "@([a-zA-Z0-9_]+)" // Regex pattern
func parse(match: NSTextCheckingResult, in text: String) -> CustomElementData? {
guard let range = Range(match.range(at: 1), in: text) else { return nil }
let username = String(text[range])
return CustomElementData(
type: "mention",
rawText: "@\(username)",
payload: ["username": username]
)
}
}class MentionViewProvider: MarkdownCustomViewProvider {
let supportedType = "mention"
func createView(
for data: CustomElementData,
configuration: MarkdownConfiguration,
containerWidth: CGFloat
) -> UIView {
let label = UILabel()
label.text = data.rawText
label.textColor = .systemBlue
label.font = configuration.bodyFont
label.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.1)
label.layer.cornerRadius = 4
label.sizeToFit()
return label
}
func calculateSize(
for data: CustomElementData,
configuration: MarkdownConfiguration,
containerWidth: CGFloat
) -> CGSize {
let text = data.rawText as NSString
let size = text.size(withAttributes: [.font: configuration.bodyFont])
return CGSize(width: size.width + 8, height: size.height + 4)
}
}class MentionActionHandler: MarkdownCustomActionHandler {
let supportedType = "mention"
func handleTap(data: CustomElementData, sourceView: UIView, presentingViewController: UIViewController?) {
guard let username = data.payload["username"] else { return }
print("Navigate to user profile: \(username)")
}
}let manager = MarkdownCustomExtensionManager.shared
manager.register(parser: MentionParser())
manager.register(viewProvider: MentionViewProvider())
manager.register(actionHandler: MentionActionHandler())| Extension | Syntax | Description |
|---|---|---|
| Video | [video:filename] |
Embed video with QuickLook playback |
| Mermaid | ```mermaid |
Render Mermaid diagrams with a custom code block renderer |
| ECharts | <echarts height="320">JSON</echarts> |
Render ECharts with an HTML-style custom tag |
| Mention* | @username |
User mention (example) |
| Emoji* | ::emoji_name:: |
Custom emoji (example) |
Video, Mermaid, and ECharts are implemented in the demos. Mention and Emoji only illustrate the extension protocols and do not have bundled implementations.
In addition to inline syntax extensions, you can also create custom code block renderers for specific languages:
public final class MermaidRenderer: MarkdownCodeBlockRenderer {
public let supportedLanguage = "mermaid"
public func renderCodeBlock(
code: String,
configuration: MarkdownConfiguration,
containerWidth: CGFloat
) -> UIView {
// Use WKWebView to render Mermaid diagrams
let view = MermaidWebView(code: code, frame: ...)
return view
}
public func calculateSize(
code: String,
configuration: MarkdownConfiguration,
containerWidth: CGFloat
) -> CGSize {
// Estimate height based on diagram type
return CGSize(width: containerWidth - 32, height: estimatedHeight)
}
}let manager = MarkdownCustomExtensionManager.shared
manager.register(codeBlockRenderer: MermaidRenderer())Supported Diagram Types (via Mermaid.js):
- Flowchart (flowchart/graph)
- Sequence Diagram (sequenceDiagram)
- Class Diagram (classDiagram)
- State Diagram (stateDiagram)
- Gantt Chart (gantt)
- Mind Map (mindmap)
The ECharts example uses an HTML-style tag, but it is still recognized by MarkdownCustomParser and rendered by a MarkdownCustomViewProvider that returns a WKWebView. It does not enable general-purpose HTML or arbitrary <script> rendering.
Register it in AppDelegate:
MarkdownCustomExtensionManager.shared.registerEChartsExtension()Pass an ECharts option as pure JSON:
<echarts height="320">
{
"xAxis": { "type": "category", "data": ["Mon", "Tue", "Wed"] },
"yAxis": { "type": "value" },
"series": [{ "type": "bar", "data": [120, 200, 150] }]
}
</echarts>height is optional and defaults to 320pt; the example clamps it to 220β640pt. The configuration must be a JSON object and cannot contain JavaScript functions. Invalid JSON, script loading failures, and rendering failures produce a visible error message. The current demos cover:
- Bar, pie, line, and scatter charts
- Stacked area, candlestick, and histogram charts
- Graph and heatmap charts
The ECharts and Mermaid examples load their scripts from a CDN, so the first render requires network access. For fully offline products, bundle a fixed JavaScript version with the app and update the corresponding demo renderer to load the local resource.
Problem: Build fails when using swift build on macOS
Solution: This library only supports iOS platform, must be built in Xcode targeting iOS simulator or device
Problem: Images in Markdown don't display
Causes:
- Image URL is invalid or inaccessible
- Network permissions not configured
Solutions:
- Check network permission configuration in Info.plist
- Use valid image URLs
Problem: Sendable-related warnings appear
Solution: Library is built with Swift 5.9 to avoid strict concurrency checking
The ExampleForMarkdown app now includes a Theme Gallery with four complete Markdown themes: Parchment, Sage, Midnight, and Plum. Selecting a theme stores the choice in UserDefaults and reuses the same configuration in the Markdown preview, AI Chat and its history, the long-history example, TableView Streaming, and both smart-streaming examples.
Open Theme Gallery from the example app, select a theme card, and then enter any of the pages above to inspect the result. Theme persistence is implemented only by the Demo's MarkdownDemoThemeStore; it is not a global SDK singleton. Pages read the selected theme when they are created, so reopen an already visible page after changing the theme.
The complete theme definitions are available in MarkdownThemeGalleryViewController.swift. Product apps can use the same pattern: keep the selected theme in app state, create one complete MarkdownConfiguration, and assign it to every Markdown view that should share the theme.
Colors and block surfaces can be configured independently. MarkdownBlockAppearance draws its corner radius and border with CALayer; it does not change constraints, padding, measured height, or the scroll range.
var configuration = MarkdownConfiguration.default
// Text and surface colors
configuration.textColor = .label
configuration.headingColor = .label
configuration.linkColor = .systemIndigo
configuration.codeTextColor = .label
configuration.codeBackgroundColor = .secondarySystemBackground
configuration.blockquoteTextColor = .label
configuration.blockquoteBarColor = .systemIndigo
configuration.blockquoteBackgroundColor = .secondarySystemBackground
configuration.tableBorderColor = .separator
configuration.tableHeaderBackgroundColor = .systemIndigo.withAlphaComponent(0.12)
configuration.tableRowBackgroundColor = .systemBackground
configuration.tableAlternateRowBackgroundColor = .secondarySystemBackground
// Formula glyphs/rules and formula surface
configuration.latexTextColor = .label
configuration.latexBackgroundColor = .secondarySystemBackground
// Visual-only block appearance
configuration.codeBlockAppearance = MarkdownBlockAppearance(
cornerRadius: 14,
borderWidth: 1,
borderColor: .separator
)
configuration.blockquoteAppearance = MarkdownBlockAppearance(
cornerRadius: 12,
borderWidth: 1,
borderColor: .separator
)
configuration.tableAppearance = MarkdownBlockAppearance(
cornerRadius: 12,
borderWidth: 1,
borderColor: .separator
)
configuration.imageAppearance = MarkdownBlockAppearance(
cornerRadius: 14 // Image borders remain opt-in.
)
configuration.latexAppearance = MarkdownBlockAppearance(
cornerRadius: 12,
borderWidth: 1,
borderColor: .separator
)
configuration.detailsAppearance = MarkdownBlockAppearance(
cornerRadius: 12,
borderWidth: 1,
borderColor: .separator
)
markdownView.configuration = configurationlatexTextColor is the default color for formula glyphs and fraction/radical rules. An explicit LaTeX \color{...} command still takes precedence. Image themes default to rounded corners without a border; set borderWidth and borderColor only when a product specifically needs an image outline.
When a screen uses MarkdownRenderer.prepare(_:), give the renderer and the destination view the same configuration. This prevents cached/prepared content from retaining colors from another theme.
let renderer = MarkdownRenderer(
configuration: configuration,
containerWidth: contentWidth
)
let preparedContent = renderer.prepare(markdown)
markdownView.configuration = configuration
markdownView.setPreparedContent(preparedContent)- π¨ Four Demo Themes and Theme Gallery - Added Parchment, Sage, Midnight, and Plum theme previews, with Demo-only
UserDefaultspersistence and consistent selection across the Markdown preview, AI Chat/history, long-history, TableView Streaming, and smart-streaming examples. - π§± Configurable Block Appearance - Added corner-radius and border configuration for code blocks, blockquotes, tables, images, LaTeX, and details blocks. These
CALayer-only settings do not participate in height measurement or change the scroll range. - β Theme-Aware Formula Rendering - Added
latexTextColorso formula glyphs and drawing rules follow the selected theme while explicit LaTeX\color{...}values continue to take precedence. - π Consistent Prepared-Content Styling - Demo screens now pass the selected configuration to both
MarkdownRendererand their Markdown views, preventing cached chat/history content from using stale theme colors. - πΌ Cleaner Image Defaults - Demo themes keep image rounding but leave image borders disabled by default; borders remain available as an opt-in appearance setting.
- π Backpressured Smart-Streaming Pipeline - SmartBuffer now releases safe completed prefixes incrementally, parses modules serially off the main thread, and applies view creation under per-frame and Typewriter high/low-watermark budgets. Input order, UI order, and drain completion remain deterministic even when parsing outruns playback.
- β‘ Display-Link Typewriter Scheduling - Replaced recursive delayed ticks with a 30 FPS
CADisplayLinktimeline. Punctuation delays are precomputed in UTF-16 coordinates, pending work uses an O(1) FIFO head, and a catch-up frame performs at most one reveal/layout callback. - π Incremental Height Cache - Streaming height now grows from known root visibility and text deltas instead of repeatedly fitting the complete
UIStackView. Same-width intrinsic-size reads are cached, height notifications are coalesced, and structural changes still fall back to full reconciliation. - β¨ Stable Rendering Without Repaint Flashes - TextKit views only invalidate drawing when their real bounds change. Smart-stream table updates are serialized/coalesced with a guaranteed final flush, preventing overlapping self-sizing batches from repainting already displayed content.
- π§± Bounded Rich-Block Layout Work - Tables reuse stable geometry, quotes are revealed as atomic blocks, and layout-driven height invalidation only runs after an actual width change. Rich Markdown no longer amplifies full-document layout work as the stream grows.
- π Deterministic Module and Extension Handling - Complete modules preserve global ordering and document-wide heading IDs. Fenced code and opaque custom blocks remain intact across chunk boundaries, and custom streaming tags stay explicitly opt-in through
streamingBlockTagName. - π§Ή Smart-Streaming API and Demo Cleanup - Smart-stream usage is consolidated around
beginRealStreaming(),appendStreamData(_:), andendRealStreaming(completion:); the pre-splitappendBlockpath and unused streaming demo controls were removed. - π Opt-In Performance Diagnostics and Regression Coverage - Added
[MDPERF]aggregate diagnostics viaMD_STREAM_PERF_LOG=1/MD_STREAM_PERF_ONLY=1, plus coverage for Unicode punctuation, emoji boundaries, FIFO ordering, backpressure, redraw deduplication, height caching, and final drain behavior. The merged baseline passed 68 iOS Simulator tests and SwiftPM/CocoaPods example builds.
- π Thread-Safe Custom Extension Registry - Parser, view-provider, action-handler, and code-block-renderer registration and lookup are now synchronized. Third-party parser callbacks execute outside the registry lock to avoid re-entrant deadlocks.
- πΌ Unified Kingfisher Image Pipeline - Removed the redundant in-house memory/disk image cache and routed loading, caching, request cancellation, and cache hits through Kingfisher.
- β‘ Single-Pass LaTeX Rendering - A formula is parsed once into a reusable render result shared by measurement, attachment layout, and view creation, eliminating duplicate parse work without introducing artificial IDs.
- π§© Modularized Markdown Renderer - Split the monolithic
MarkdownDisplayView.swiftinto focusedMarkdownViewTextKitextension files while preserving the public API and existing rendering behavior.
- π Fixed Initial Details-Block Whitespace - Long ordered lists no longer leave a large blank area before the following heading or collapsible
<details>block when a Markdown screen first appears. The list wrapper is now constrained to its actual content height instead of being allowed to stretch vertically. - π Synchronized Deferred Layout Without User Interaction - After off-screen elements are appended, the Markdown view now propagates its final height through the outer scroll view's
contentLayoutGuideandcontentSize; users no longer need to swipe once to correct the layout. - π Preserved Scroll Position During Append-Only Rendering - Deferred elements appended below the current viewport no longer add their entire height to
contentOffset, preventing incorrect jumps and stale-offset clamping during the first render.
- β‘ Incremental Stream Buffer Scanning -
MarkdownStreamBuffer.append()now scans only the uncommitted tail instead of rescanning the full accumulated text, eliminating O(nΒ²) growth; measured 1.6x-3.8x speedup on long streaming input. Added chunk-boundary-independence differential tests to lock the behavior invariant. - β‘ TextKit 2 Incremental Layout & Height Measurement - Typewriter append no longer replaces the whole attributed string per character; edits happen incrementally within an editing transaction. Height measurement now uses
usageBoundsForTextContainer, removing the O(nΒ²) bottleneck during streaming append. - β‘ LaTeX Formula Parse Deduplication - Reduced repeated parsing of a single formula from 6 times to 1;
LatexMathViewnow short-circuits when content is unchanged. - π Fixed Three Rendering Regressions Surfaced After Code Review Merge - Restored correct container width semantics, fixed content truncation caused by
draw(_:)dirty-rect clipping, and added a fade-in transition when off-screen placeholders are replaced to avoid flicker. - π Eliminated Background-Thread UIKit Access - Container width is now snapshotted on the main thread before background parsing begins, removing a potential crash risk.
- π Streaming Auto-Scroll Improvements - Added user takeover detection and throttling so scrolling back to read history is no longer forced back to the bottom.
- β‘ Typewriter Watchdog Uses a Persistent Timer - Avoids rebuilding a Timer on every step, and stays reliable during scrolling via
.commonRunLoop mode. - β‘ Regex Cache Coverage - Details block and code-highlighting regex matching now consistently use the existing
cachedRegex. - π§Ή Code Cleanup - Removed dead incremental-parsing code with zero call sites across the repo; gated 251 debug log statements behind
#if DEBUGto reduce release-build logging overhead. - β¨ Enhanced AI Chat Examples - CocoaPods/SPM examples now include chat history support with refined interaction details.
- π Append Typewriter Height Stability - Character reveal is now separated from height remeasurement. Layout callbacks fire only when height actually changes, reducing row jitter during streaming playback.
- π§± No Pre-Reveal Blank Height - Append mode discards precalculated final height before typing starts, so cells no longer flash a large empty area, and height only grows with visible text.
- π Height Floor During Playback - While append typewriter is active, height is not allowed to shrink on transient width corrections, preventing bubble bounce; the floor is released after playback finishes or the engine stops so wider reflows can still settle correctly.
- π§ͺ Streaming Layout Tests - Added coverage for reveal/height decoupling, pre-playback height reset, and height-floor release on finish/stop.
- π Stable Real-Streaming Rendering - Starting real streaming now cancels and invalidates pending regular renders, preventing stale parse results from replacing the active typewriter UI.
- π§± Atomic Block Reveal - Tables, code blocks, images, LaTeX, details, thematic breaks, and custom views are revealed as complete blocks with their final height instead of expanding from a temporary
1ptplaceholder. - π Chat Auto-Follow and Row-Height Fixes - The SPM and CocoaPods AI chat examples now coalesce row-height updates, keep following the streaming message after layout changes, and pause auto-scroll while the user browses older messages.
- β¨ Reduced Streaming Cell Flicker - Offscreen deltas no longer reload cells repeatedly, reused streaming cells resume from accumulated content, and the final state waits for the typewriter queue to finish before switching to static rendering.
- πΌ Kingfisher Image Loading - Switched Markdown image loading and caching to Kingfisher 8.9.0 in
ImageView.swift. - π¦ Dependency Alignment - Added Kingfisher to both
Package.swiftandMarkdownDisplayKit.podspec, so SPM and CocoaPods resolve the same image library. - π§ͺ Example Update - Updated
ExampleForMarkdownandCocoapodsMDExampleimage views to use Kingfisher-based loading.
- π Prepared Content Rendering - Added
MarkdownRenderer.prepare(_:)andMarkdownViewTextKit.setPreparedContent(_:)so apps can pre-parse long Markdown off the main display path and reuse the generated render elements. - π Precomputed Height Fast Path - Prepared content carries estimated element heights, allowing text/heading views to skip expensive first-pass TextKit height calculation when the width is known.
- π§ͺ History Markdown Example Optimization -
CocoapodsMDExamplenow pre-renders historical long Markdown messages in the background and uses cached row heights to reduce first-scroll stutter. - π History Row Blank-Space Fix - Removed the oversized initial row-height placeholder and fixed callback ordering so measured content height replaces estimates correctly.
- π Height Measurement Stabilization - Hardened
notifyHeightChangewith width fallback, frame-height fallback, and transient-zero suppression to avoid0 β actual heightjumps during initial layout or rapid updates. - π Paragraph-Level Streaming Fallback - Real streaming now emits single-heading or heading-less Markdown by paragraph boundaries when heading-based segmentation is unavailable, while skipping fenced code blocks.
- π Whole-List Top/Bottom Padding - Added
listTopPaddingandlistBottomPaddingso the entire list wrapper can apply configurable top/bottom spacing without changing per-item layout.
- β
isPlainText()Detection - AddedisPlainText()inMarkdownStreamBufferto identify non-Markdown content. - β‘ Faster Plain-Text Output - For plain text without Markdown markers, modules can now be submitted at
\nboundaries instead of requiring\n\n, enabling faster typewriter output. - β
Markdown Flow Unchanged - Markdown content behavior is unchanged and still waits for
\n\nparagraph boundaries.
- π Ordered List Height Consistency Fix - Fixed an issue where the first ordered-list item could be stretched taller than following items in some stack/reuse layouts.
- π§± List Layout Constraint Hardening - Adjusted list wrapper constraints (
bottom <=) and strengthened vertical hugging/compression priorities to prevent extra height from being absorbed by the first item. - π§Ή List Content Normalization - Added normalization/cleanup for invisible list text nodes (leading/trailing newlines, zero-width/control whitespace) to avoid phantom height.
- π Markdown Table Column Alignment - Added support for table alignment syntax (
:---,:---:,---:) and applied alignment per column. - π Malformed Table Auto-Fix - Added
autoFixMalformedTables(default:true) to normalize common broken table output (isolated|, accidental blank lines inside table blocks). - βοΈ Configurable Line Spacing - Added
lineSpacingconfiguration forbody,heading,quote,codeBlock, replacing fixed line spacing constants. - π Table Link Tap Callback - Table cells keep using
UILabelfor better scrolling performance; link tap now routes through table cell selection and triggers existingonLinkTap. - π Touch Routing Fix - Fixed gesture conflict where outer TextKit tap handling could swallow table attachment touches.
β οΈ Configuration Cleanup - Removed table-level alignment override config; table text alignment now follows Markdown table syntax (fallback: left).
- π Link Underline Control - Added
linkUnderlineEnabledconfiguration option to control whether links display underlines- New property
linkUnderlineEnabled: BoolinMarkdownConfiguration(default:true) - Affects all link types: inline Markdown links (
[text](url)) and TOC navigation links - Root cause fix: Implemented
NSTextLayoutManagerDelegate.renderingAttributesForLink(_:at:defaultAttributes:)to properly intercept TextKit 2's built-in link rendering pipeline, which previously ignoredNSAttributedStringunderline attributes entirely
- New property
- π Code Block Horizontal Scrolling - Code blocks now support horizontal scrolling to view complete long code lines
- Implemented using
NSTextAttachmentViewProviderpattern, consistent with LaTeX formula and table rendering architecture - New
CodeBlockAttachmentandCodeBlockAttachmentViewProviderclasses for code block rendering - Code text no longer wraps; users can scroll horizontally to view full code content
- Maintains original syntax highlighting, background color, and corner radius styling
- Implemented using
- π³ Haptic Feedback Timing Optimization - Haptic feedback now syncs precisely with TypewriterEngine output rhythm
- Text haptics: Only triggers when
revealCharacteractually displays new characters - Block haptics: Triggers when block element animation completes (image, LaTeX, etc.)
- Removed unnecessary haptics for container views (
.show) and small elements (.label) - Haptic feedback no longer triggers on data arrival, but on actual content display
- Text haptics: Only triggers when
- π³ Streaming Haptic Feedback - Added haptic feedback support during streaming output for enhanced user experience
- New
StreamingHapticFeedbackStyleenum with options:.none,.light,.medium,.heavy,.soft,.rigid - New configuration options:
streamingHapticFeedbackStyle(feedback intensity) andstreamingHapticMinInterval(minimum interval) - Supports smart streaming (
appendStreamData) and fake streaming (startStreaming) modes
- New
- π¨ Comprehensive Configuration Options - Added extensive customization for all Markdown elements:
- LaTeX Formula:
latexFontSize,latexAlignment(left/center/right),latexBackgroundColor,latexPadding - Blockquote:
blockquoteBackgroundColor,blockquoteBarWidth,blockquoteContentSpacing,blockquoteContentPadding - Table:
tableMinColumnWidth,tableMaxColumnWidth,tableRowHeight,tableCellPadding,tableSeparatorHeight - List:
listItemSpacing,listMarkerMinWidth,listMarkerSpacing - Details:
detailsSummaryFont,detailsSummaryTextColor,detailsSummaryMinHeight,detailsContentPadding,detailsSpacing - Syntax Highlighting:
syntaxColors,syntaxColorsDarkwithSyntaxHighlightColorsstruct (keyword, string, number, comment, type, function, property, preprocessor) - TOC:
tocTextColor
- LaTeX Formula:
- π Bug Fix -
tableRowBackgroundColornow properly applied to table rows - π Documentation - Updated README with complete configuration options
- π Typewriter Append - Add
.appendmode with throttled height updates to reduce layout jumps during cell streaming - βοΈ Streaming Config - Expose
typewriterTextMode,typewriterHeightUpdateInterval,streamMinModuleLength - π§Ή Memory Cleanup - Add cache clearing helpers and Mermaid WebView cleanup to reduce retained memory
- π§ͺ Example Update - AI chat stream uses safer LaTeX normalization (code regions ignored) and recommended config
- π Docs Update - Refresh README content
- π SPM Fix - Fix simulator build error in Swift Package Manager example project
- π Crash Fix - Serialize
swift-markdownparsing to avoidcmark_parser_attach_syntax_extensionrace crash in concurrent renders - π§Ή Reuse Safety - Add
resetForReuse()to clear internal caches/state forUITableViewCellreuse scenarios - π§ͺ Example Update - Add crash reproduction screen and incremental row insert demo for table view usage
- π Bug Fix - Fixed potential crash when processing Unicode characters (emoji, CJK characters) in streaming mode
MarkdownStreamBuffer.extractModule: Use safe string index withlimitedByto prevent out-of-bounds crashTypewriterEngine.calculateDelay: Use safe string index to prevent crash when calculating delay for special characters
- π Real Streaming Support - New
MarkdownStreamBufferfor intelligent real-time streaming from network/LLM APIs- Smart module detection: automatically detects complete Markdown blocks (headings, code blocks, tables, LaTeX)
- Handles incomplete structures: waits for closing tags before rendering (e.g., unclosed ``` or $$)
- Incremental rendering: renders complete modules immediately while buffering incomplete content
- π« Smart Waiting Indicator - In real streaming mode, automatically shows waiting animation when TypewriterEngine queue is empty and no network data arrives
- ποΈ Code Refactoring - Extracted
MarkdownTextViewTK2,MarkdownStreamBuffer, andTypewriterEngineinto separate files for better maintainability - π Streaming Fixes - Multiple fixes for real streaming mode stability and rendering issues
- π Bug Fix - Fixed code blocks not rendering properly in real streaming mode when content arrives in multiple chunks
- π Instant Loading - Significantly optimized loading speed with ultra-fast first screen rendering
- β‘ CPU Optimization - Streaming mode with nested style rendering now uses much less CPU (iPhone 17 Pro simulator peak < 56%, average 30%)
- π Enhanced Custom Extensions - New
MarkdownCodeBlockRendererprotocol for custom code block rendering (e.g., Mermaid diagrams) - π¨ Mermaid Support - Example project now includes Mermaid diagram renderer supporting flowcharts, mind maps, and more
- π Initial release
- β Full Markdown syntax support
- β 20+ language code highlighting
- β Automatic table of contents generation
- β Dark mode support
- β High-performance asynchronous rendering
Issues and Pull Requests are welcome!
Before submitting a PR, please ensure:
- Code compiles successfully
- Follows existing code style
- Adds necessary tests
This project is licensed under the MIT License - see the LICENSE file for details.
MarkdownDisplayView is created and maintained by @zjc19891106. If this library saved you time, consider supporting me. Thanks to everyone who has supported me so far.
- swift-markdown - Markdown parsing library
- Kingfisher - Image loading and caching library
- KaTeX - Math formula rendering fonts
- Apple TextKit 2 - High-performance text rendering framework
- Gemini3 Pro&Claude&Grok&GPT
- All contributors and users
- All friends who provided suggestions and feedback
If you have questions or suggestions, please contact via:
-
Submit GitHub Issue
-
Send email to: 984065974@qq.com or luomobancheng@gmail.com
**If you find this project helpful, please give it a Star βοΈ for support!









