Skip to main content

Overview

While MarkdownView is built with UIKit/AppKit, you can easily use it in SwiftUI by wrapping MarkdownTextView 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

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)
        }
    }
}

Key Implementation Details

1

Clear Background

Set the background to clear so it respects SwiftUI’s environment:
view.backgroundColor = .clear
view.isOpaque = false
2

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)
}
3

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

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)
            }
        }
    }
}
Usage:
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
}
Usage:
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, use NSViewRepresentable 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.

Build docs developers (and LLMs) love