A WKWebView-based Markdown renderer for iOS. Converts Markdown to HTML using markdown-it with syntax highlighting by highlight.js.
Features
- Renders Markdown as styled HTML inside a native
UIView - Syntax highlighting for code blocks via highlight.js
- Dark mode support (automatic, via
prefers-color-scheme) - Custom CSS injection
- markdown-it plugin support (e.g., KaTeX math)
- External stylesheet loading
- Intrinsic content size for Auto Layout integration
- Link tap handling
- SwiftUI support via
MarkdownUI
Requirements
| Target | Version |
|---|---|
| iOS | >= 16.0 |
| Swift | >= 6.0 |
Installation
MarkdownView is available through Swift Package Manager.
Swift Package Manager
dependencies: [ .package(url: "https://github.com/keitaoouchi/MarkdownView.git", from: "2.1.0") ]
Alternatively, you can add the package directly via Xcode (File > Add Package Dependencies).
Quick Start
UIKit
import MarkdownView let md = MarkdownView() md.reconfigure() md.render(markdown: "# Hello World!")
SwiftUI
import SwiftUI import MarkdownView struct ContentView: View { var body: some View { ScrollView { MarkdownUI(body: "# Hello World!") .onTouchLink { request in print(request.url ?? "") return false } .onRendered { height in print(height) } } } }
API Reference
MarkdownView (UIKit)
Initializers
| Signature | Description |
|---|---|
init() |
Creates a view with default settings. Call reconfigure() then render(markdown:) to display content. |
init(css: String?, plugins: [String]?, stylesheets: [URL]? = nil, styled: Bool = true) |
Pre-configures a web view with CSS, plugins, and stylesheets. Use with render(markdown:) for efficient updates. |
init?(coder: NSCoder) |
Interface Builder support. |
Properties
| Name | Type | Default | Description |
|---|---|---|---|
isScrollEnabled |
Bool |
true |
Controls whether the internal web view scrolls. Set false when embedding in a UIScrollView. |
onTouchLink |
((URLRequest) -> Bool)? |
nil |
Called when a link is tapped. Return true to allow navigation, false to cancel. |
onRendered |
((CGFloat) -> Void)? |
nil |
Called when rendering completes. The parameter is the content height in points. |
intrinsicContentSize |
CGSize |
— | Returns the measured content height. Updates automatically after rendering. |
Methods
| Signature | Description |
|---|---|
reconfigure(css: String? = nil, plugins: [String]? = nil, stylesheets: [URL]? = nil, styled: Bool = true) |
Creates (or recreates) the internal web view with the given CSS, plugins, and stylesheets. |
reconfigure(with: ConfigurationOptions) |
Same as above using a ConfigurationOptions struct. |
render(markdown: String, options: RenderOptions = RenderOptions()) |
Renders Markdown on the current web view. If the web view is still loading, the request is queued and executed automatically when ready. |
reconfigure vs render: reconfigure sets up the web view and styling. render sends Markdown to the already-configured web view. For dynamic content that changes frequently, call reconfigure once and then render for each update.
Deprecated:
load(markdown:...)andshow(markdown:)still work but are deprecated. Usereconfigure+renderinstead.
MarkdownUI (SwiftUI)
Initializer
MarkdownUI( body: String? = nil, css: String? = nil, plugins: [String]? = nil, stylesheets: [URL]? = nil, styled: Bool = true )
| Parameter | Type | Default | Description |
|---|---|---|---|
body |
String? |
nil |
The Markdown string to render. |
css |
String? |
nil |
Custom CSS to inject. |
plugins |
[String]? |
nil |
Array of markdown-it plugin JavaScript strings. |
stylesheets |
[URL]? |
nil |
External stylesheet URLs to load. |
styled |
Bool |
true |
Use the built-in Bootstrap-based stylesheet. |
View Modifiers
| Modifier | Description |
|---|---|
.onTouchLink(perform: @escaping (URLRequest) -> Bool) |
Called when a link is tapped. Return true to allow navigation, false to cancel. |
.onRendered(perform: @escaping (CGFloat) -> Void) |
Called when rendering completes with the content height. |
Note:
MarkdownUIdisables internal scrolling. Wrap it in aScrollViewfor scrollable content.
Customization
Custom CSS
Inject a CSS string to override the default styles:
// UIKit let css = "body { background-color: #f0f0f0; } code { font-size: 14px; }" let md = MarkdownView(css: css, plugins: nil) md.render(markdown: "# Styled content") // SwiftUI MarkdownUI(body: "# Styled content", css: "body { background-color: #f0f0f0; }")
See Example/Example/ViewController/CustomCss.swift for a full example.
Plugins
Add markdown-it compatible plugins by passing the plugin JavaScript as a string. Each plugin must be self-contained with no external dependencies.
let katexPlugin = try! String(contentsOfFile: Bundle.main.path(forResource: "katex", ofType: "js")!) let md = MarkdownView(css: nil, plugins: [katexPlugin]) md.render(markdown: "Inline math: $E = mc^2$")
See Example/Example/ViewController/Plugins.swift for a full example, and the sample plugin project for building a compatible plugin library.
External Stylesheets
Load CSS from remote URLs:
let url = URL(string: "https://example.com/custom.css")! let md = MarkdownView(css: nil, plugins: nil, stylesheets: [url]) md.render(markdown: "# Remote-styled content")
Styled vs Non-Styled Mode
By default, styled: true loads a Bootstrap-based stylesheet with highlight.js themes. Set styled: false to start with a blank canvas and apply your own CSS from scratch.
let md = MarkdownView(css: myCustomCSS, plugins: nil, styled: false)
Dark Mode
The built-in stylesheet supports dark mode automatically via prefers-color-scheme. Text and link colors adapt to the system appearance. No additional configuration is needed.
To customize dark mode styles, inject CSS with a prefers-color-scheme media query:
let css = """ @media (prefers-color-scheme: dark) { body { background-color: #1a1a1a; } code { color: #e06c75; } } """ let md = MarkdownView(css: css, plugins: nil)
Example Project
The Example/ directory contains a full iOS app demonstrating UIKit usage, custom CSS, and plugin integration.
Architecture
See AGENTS.md for a high-level overview of the component architecture and data flow.
License
bootstrap is licensed under the MIT License. highlight.js is licensed under the BSD-3-Clause License. markdown-it is licensed under the MIT License.
MarkdownView is available under the MIT license. See the LICENSE file for more info.
