Overview
While MarkdownView is built with UIKit/AppKit, you can easily use it in SwiftUI by wrappingMarkdownTextView in a UIViewRepresentable (iOS/visionOS) or NSViewRepresentable (macOS).
Complete SwiftUI Wrapper
Here’s a production-ready wrapper for iOS and visionOS:import SwiftUI
import MarkdownParser
import MarkdownView
struct MarkdownTextViewRepresentable: UIViewRepresentable {
let markdown: String
var theme: MarkdownTheme = .default
func makeUIView(context: Context) -> MarkdownTextView {
let view = MarkdownTextView()
view.backgroundColor = .clear
view.isOpaque = false
return view
}
func updateUIView(_ uiView: MarkdownTextView, context: Context) {
uiView.theme = theme
let parser = MarkdownParser()
let result = parser.parse(markdown)
let content = MarkdownTextView.PreprocessedContent(
parserResult: result,
theme: theme
)
uiView.setMarkdown(content)
}
func sizeThatFits(
_ proposal: ProposedViewSize,
uiView: MarkdownTextView,
context: Context
) -> CGSize? {
guard let width = proposal.width, width > 0 else {
return uiView.intrinsicContentSize
}
let measuredSize = uiView.boundingSize(for: width)
return CGSize(width: width, height: measuredSize.height)
}
}
Using the Wrapper
- Basic Usage
- With Custom Theme
- Dynamic Content
struct ContentView: View {
private let markdown = """
# Hello, SwiftUI!
This `MarkdownTextView` is wrapped with `UIViewRepresentable`.
struct GreetingView: View {
var body: some View {
Text("Hello from MarkdownView")
}
}
"""
var body: some View {
ScrollView {
MarkdownTextViewRepresentable(markdown: markdown)
.padding(.horizontal)
}
}
}
struct ContentView: View {
@State private var theme: MarkdownTheme = .default
var body: some View {
ScrollView {
MarkdownTextViewRepresentable(
markdown: "# Custom Theme\n\nStyled content...",
theme: theme
)
.padding(.horizontal)
}
.toolbar {
Button("Toggle Theme") {
theme = createCustomTheme()
}
}
}
func createCustomTheme() -> MarkdownTheme {
var theme = MarkdownTheme()
theme.scaleFont(by: .large)
return theme
}
}
struct ContentView: View {
@State private var markdownText = "# Loading..."
var body: some View {
ScrollView {
MarkdownTextViewRepresentable(markdown: markdownText)
.padding(.horizontal)
}
.task {
// Fetch markdown from network
markdownText = await fetchMarkdown()
}
}
func fetchMarkdown() async -> String {
// Simulate network request
try? await Task.sleep(nanoseconds: 1_000_000_000)
return "# Fresh Content\n\nLoaded from server!"
}
}
Key Implementation Details
Clear Background
Set the background to clear so it respects SwiftUI’s environment:
view.backgroundColor = .clear
view.isOpaque = false
Parse in updateUIView
Always parse and update content in
updateUIView(_:context:) to ensure the view stays in sync with SwiftUI state:func updateUIView(_ uiView: MarkdownTextView, context: Context) {
uiView.theme = theme
let parser = MarkdownParser()
let result = parser.parse(markdown)
let content = MarkdownTextView.PreprocessedContent(
parserResult: result,
theme: theme
)
uiView.setMarkdown(content)
}
Implement sizeThatFits
Provide accurate size measurements for SwiftUI layout:
func sizeThatFits(
_ proposal: ProposedViewSize,
uiView: MarkdownTextView,
context: Context
) -> CGSize? {
guard let width = proposal.width, width > 0 else {
return uiView.intrinsicContentSize
}
let measuredSize = uiView.boundingSize(for: width)
return CGSize(width: width, height: measuredSize.height)
}
Advanced Features
Link Handling
Add link handling by creating a coordinator:struct MarkdownTextViewRepresentable: UIViewRepresentable {
let markdown: String
var onLinkTap: ((URL) -> Void)?
func makeCoordinator() -> Coordinator {
Coordinator(onLinkTap: onLinkTap)
}
func makeUIView(context: Context) -> MarkdownTextView {
let view = MarkdownTextView()
view.backgroundColor = .clear
view.isOpaque = false
view.linkHandler = context.coordinator.handleLink
return view
}
class Coordinator {
let onLinkTap: ((URL) -> Void)?
init(onLinkTap: ((URL) -> Void)?) {
self.onLinkTap = onLinkTap
}
func handleLink(_ payload: LinkPayload, _ range: NSRange, _ point: CGPoint) {
if case .url(let url) = payload {
onLinkTap?(url)
}
}
}
}
MarkdownTextViewRepresentable(markdown: content) { url in
// Handle link tap
UIApplication.shared.open(url)
}
Image Tap Handling
Extend the coordinator to handle image taps:var onImageTap: ((String) -> Void)?
func makeUIView(context: Context) -> MarkdownTextView {
let view = MarkdownTextView()
view.backgroundColor = .clear
view.isOpaque = false
view.linkHandler = context.coordinator.handleLink
view.imageTapHandler = context.coordinator.handleImageTap
return view
}
class Coordinator {
// ... existing code ...
func handleImageTap(_ source: String, _ point: CGPoint) {
onImageTap?(source)
}
}
Theme from Environment
You can create a custom environment key to pass themes through the SwiftUI view hierarchy:private struct MarkdownThemeKey: EnvironmentKey {
static let defaultValue = MarkdownTheme.default
}
extension EnvironmentValues {
var markdownTheme: MarkdownTheme {
get { self[MarkdownThemeKey.self] }
set { self[MarkdownThemeKey.self] = newValue }
}
}
struct MarkdownTextViewRepresentable: UIViewRepresentable {
let markdown: String
@Environment(\.markdownTheme) var theme
// ... rest of implementation using theme from environment
}
VStack {
MarkdownTextViewRepresentable(markdown: content)
}
.environment(\.markdownTheme, customTheme)
Performance Considerations
Parsing markdown can be expensive for large documents. Consider these optimizations:
Debounce Updates
For text editors, debounce markdown updates:struct ContentView: View {
@State private var markdown = ""
@State private var debouncedMarkdown = ""
var body: some View {
VStack {
TextEditor(text: $markdown)
.frame(height: 200)
MarkdownTextViewRepresentable(markdown: debouncedMarkdown)
}
.onChange(of: markdown) { oldValue, newValue in
// Debounce updates
Task {
try? await Task.sleep(nanoseconds: 300_000_000) // 300ms
if markdown == newValue {
debouncedMarkdown = newValue
}
}
}
}
}
Cache Parsed Content
For static or infrequently changing content, cache the preprocessed content:class MarkdownCache {
private var cache: [String: MarkdownTextView.PreprocessedContent] = [:]
private let parser = MarkdownParser()
func content(for markdown: String, theme: MarkdownTheme) -> MarkdownTextView.PreprocessedContent {
let key = "\(markdown)-\(theme.hashValue)"
if let cached = cache[key] {
return cached
}
let result = parser.parse(markdown)
let content = MarkdownTextView.PreprocessedContent(
parserResult: result,
theme: theme
)
cache[key] = content
return content
}
}
macOS Support
For macOS applications, useNSViewRepresentable instead:
import AppKit
struct MarkdownTextViewRepresentable: NSViewRepresentable {
let markdown: String
var theme: MarkdownTheme = .default
func makeNSView(context: Context) -> MarkdownTextView {
let view = MarkdownTextView()
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.clear.cgColor
return view
}
func updateNSView(_ nsView: MarkdownTextView, context: Context) {
nsView.theme = theme
let parser = MarkdownParser()
let result = parser.parse(markdown)
let content = MarkdownTextView.PreprocessedContent(
parserResult: result,
theme: theme
)
nsView.setMarkdown(content)
}
func sizeThatFits(
_ proposal: ProposedViewSize,
nsView: MarkdownTextView,
context: Context
) -> CGSize? {
guard let width = proposal.width, width > 0 else {
return nsView.intrinsicContentSize
}
let measuredSize = nsView.boundingSize(for: width)
return CGSize(width: width, height: measuredSize.height)
}
}
The API is identical between iOS and macOS, just swap
UIViewRepresentable for NSViewRepresentable and update the view setup accordingly.