SwiftUI Renderer Guide
September 16, 2025 ยท View on GitHub
Render Markdown as native SwiftUI views on iOS 17.0+/macOS 14.0+.
Basic Usage
import SwiftUI
import SwiftMarkdownParser
struct ContentView: View {
let markdown = "# Hello **SwiftUI**!"
@State private var content: AnyView?
var body: some View {
Group {
if let content = content {
content
} else {
ProgressView()
}
}
.task {
await loadMarkdown()
}
}
private func loadMarkdown() async {
do {
let parser = SwiftMarkdownParser()
let ast = try await parser.parseToAST(markdown)
let renderer = SwiftUIRenderer()
let view = try await renderer.render(document: ast)
await MainActor.run {
self.content = view
}
} catch {
await MainActor.run {
self.content = AnyView(Text("Error loading markdown"))
}
}
}
}
Basic Usage
Parser Configuration
For SwiftUI rendering, parser configuration affects both parsing performance and rendering behavior:
// Default configuration suitable for most SwiftUI apps
let defaultConfig = SwiftMarkdownParser.Configuration(
enableGFMExtensions: true, // Tables, task lists, etc.
strictMode: false, // Relaxed parsing
maxNestingDepth: 100, // Prevent stack overflow
trackSourceLocations: false, // Not needed for SwiftUI rendering
maxParsingTime: 30.0 // 30 second timeout
)
// Configuration for real-time preview (like in a markdown editor)
let previewConfig = SwiftMarkdownParser.Configuration(
enableGFMExtensions: true,
maxParsingTime: 2.0 // Quick timeout for responsive UI
)
// Configuration for large documents
let largeDocConfig = SwiftMarkdownParser.Configuration(
enableGFMExtensions: true,
maxParsingTime: 60.0 // Extended timeout for complex documents
)
let parser = SwiftMarkdownParser(configuration: defaultConfig)
Performance Considerations for SwiftUI
- Timeout Settings: Use shorter timeouts (2-5 seconds) for interactive editing to keep the UI responsive
- Source Locations: Usually not needed for SwiftUI rendering, disable to improve performance
- GFM Extensions: Enable for rich content, but may increase parsing time for complex tables
- Nesting Depth: Default is sufficient for most content, but may need adjustment for deeply nested lists
Creating a SwiftUI Renderer
// Default renderer
let renderer = SwiftUIRenderer()
// Custom renderer with configuration
let context = SwiftUIRenderContext(
styleConfiguration: SwiftUIStyleConfiguration(),
linkHandler: { url in
UIApplication.shared.open(url)
}
)
let renderer = SwiftUIRenderer(context: context)
Rendering Documents
// Parse and render
let parser = SwiftMarkdownParser()
let ast = try await parser.parseToAST(markdown)
let view = try await renderer.render(document: ast)
// Use in SwiftUI
struct MyView: View {
var body: some View {
ScrollView {
view
.padding()
}
}
}
Rendering Individual Nodes
// Render specific nodes
let headingNode = AST.HeadingNode(level: 1, children: [
AST.TextNode(content: "My Title")
])
let headingView = try await renderer.render(node: headingNode)
Styling and Theming
Basic Style Configuration
let styleConfig = SwiftUIStyleConfiguration(
// Typography
bodyFont: .body,
codeFont: .system(.body, design: .monospaced),
headingFonts: [
1: .largeTitle,
2: .title,
3: .title2,
4: .title3,
5: .headline,
6: .subheadline
],
// Colors
textColor: .primary,
headingColor: .primary,
linkColor: .blue,
codeTextColor: .primary,
codeBackgroundColor: Color.gray.opacity(0.1),
// Spacing
documentSpacing: 16,
paragraphSpacing: 8,
listItemSpacing: 4
)
Dark Mode Support
let darkModeConfig = SwiftUIStyleConfiguration(
textColor: .primary, // Adapts automatically
headingColor: .primary,
linkColor: .blue,
codeBackgroundColor: Color(.systemGray6), // System colors
blockQuoteBackgroundColor: Color(.systemGray6),
blockQuoteBorderColor: .blue,
tableBorderColor: Color(.systemGray4)
)
Custom Theme Example
struct BlogTheme {
static let configuration = SwiftUIStyleConfiguration(
bodyFont: .system(.body, design: .serif),
headingFonts: [
1: .system(.largeTitle, design: .serif, weight: .bold),
2: .system(.title, design: .serif, weight: .semibold),
3: .system(.title2, design: .serif, weight: .medium)
],
headingColor: Color(.systemBlue),
linkColor: Color(.systemIndigo),
codeBackgroundColor: Color(.systemGray6),
blockQuoteBackgroundColor: Color(.systemBlue).opacity(0.05),
blockQuoteBorderColor: Color(.systemBlue),
tableBorderColor: Color(.systemGray4),
tableHeaderBackgroundColor: Color(.systemGray5)
)
}
// Usage
let context = SwiftUIRenderContext(
styleConfiguration: BlogTheme.configuration
)
Responsive Design
struct ResponsiveMarkdownView: View {
let markdown: String
@Environment(\.horizontalSizeClass) var sizeClass
private var styleConfig: SwiftUIStyleConfiguration {
SwiftUIStyleConfiguration(
bodyFont: sizeClass == .compact ? .body : .title3,
documentSpacing: sizeClass == .compact ? 12 : 20,
paragraphSpacing: sizeClass == .compact ? 6 : 12
)
}
var body: some View {
MarkdownView(
markdown: markdown,
styleConfiguration: styleConfig
)
}
}
Accessibility
Built-in Accessibility Features
The SwiftUI renderer includes comprehensive accessibility support:
- VoiceOver: Proper labels and hints for all elements
- Dynamic Type: Automatic font scaling support
- Semantic Traits: Headers, links, buttons identified correctly
- Navigation: Logical reading order maintained
Accessibility Configuration
let accessibleContext = SwiftUIRenderContext(
styleConfiguration: SwiftUIStyleConfiguration(
bodyFont: .body, // Supports Dynamic Type
headingFonts: [ // Also supports Dynamic Type
1: .largeTitle,
2: .title,
3: .title2
]
),
enableAccessibility: true // Enable enhanced accessibility features
)
Custom Accessibility Labels
// The renderer automatically provides appropriate accessibility labels:
// - Headings: "Heading level 1: Title text"
// - Links: "Link: destination URL"
// - Images: "Image: alt text"
// - Code blocks: "Code block in Swift: code content"
// - Task lists: "Completed task" or "Incomplete task"
VoiceOver Testing
// Test accessibility in simulator
#if DEBUG
struct AccessibilityTestView: View {
var body: some View {
MarkdownView(markdown: """
# Accessibility Test
This is a **bold** statement with a [link](https://apple.com).
- [x] Completed task
- [ ] Pending task
""")
.accessibilityElement(children: .contain)
.accessibilityLabel("Markdown content")
}
}
#endif
Interactive Features
Link Handling
let interactiveContext = SwiftUIRenderContext(
linkHandler: { url in
// Custom link routing
if url.scheme == "myapp" {
handleDeepLink(url)
} else if url.host?.contains("internal") == true {
navigateInternally(to: url)
} else {
// Open external links
Task { @MainActor in
UIApplication.shared.open(url)
}
}
}
)
func handleDeepLink(_ url: URL) {
// Handle app-specific URLs like myapp://profile/123
if url.path == "/profile" {
// Navigate to profile
}
}
func navigateInternally(to url: URL) {
// Handle internal navigation
NavigationManager.shared.navigate(to: url)
}
Custom Image Loading
let context = SwiftUIRenderContext(
imageHandler: { url in
AnyView(
CachedAsyncImage(url: url) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxHeight: 400)
.clipShape(RoundedRectangle(cornerRadius: 8))
} placeholder: {
RoundedRectangle(cornerRadius: 8)
.fill(Color.gray.opacity(0.2))
.frame(height: 200)
.overlay(
ProgressView()
)
}
)
}
)
// Custom cached image implementation
struct CachedAsyncImage<Content: View, Placeholder: View>: View {
let url: URL
let content: (Image) -> Content
let placeholder: () -> Placeholder
var body: some View {
AsyncImage(url: url) { phase in
switch phase {
case .success(let image):
content(image)
case .failure(_):
Image(systemName: "photo")
.foregroundColor(.gray)
case .empty:
placeholder()
@unknown default:
placeholder()
}
}
}
}
Interactive Task Lists
struct InteractiveMarkdownView: View {
@State private var taskStates: [String: Bool] = [:]
var body: some View {
MarkdownView(
markdown: markdown,
onTaskToggle: { taskId, isChecked in
taskStates[taskId] = isChecked
// Save to persistence layer
saveTaskState(taskId, isChecked)
}
)
}
}
// Note: Task list interactivity would require custom renderer extension
Performance Optimization
Lazy Loading for Large Documents
struct LazyMarkdownView: View {
let markdownSections: [String]
var body: some View {
LazyVStack(spacing: 16) {
ForEach(markdownSections.indices, id: \.self) { index in
MarkdownSectionView(markdown: markdownSections[index])
.id(index)
}
}
}
}
struct MarkdownSectionView: View {
let markdown: String
@State private var renderedView: AnyView?
var body: some View {
Group {
if let view = renderedView {
view
} else {
ProgressView()
.frame(height: 100)
}
}
.onAppear {
Task {
await renderSection()
}
}
}
private func renderSection() async {
// Render only when section becomes visible
let parser = SwiftMarkdownParser()
let ast = try? await parser.parseToAST(markdown)
let renderer = SwiftUIRenderer()
let view = try? await renderer.render(document: ast!)
await MainActor.run {
self.renderedView = view
}
}
}
Memory Management
struct EfficientMarkdownView: View {
let markdown: String
@State private var content: AnyView?
@State private var task: Task<Void, Never>?
var body: some View {
Group {
if let content = content {
content
} else {
ProgressView()
}
}
.onAppear {
renderMarkdown()
}
.onDisappear {
// Cancel rendering task if view disappears
task?.cancel()
}
}
private func renderMarkdown() {
task = Task {
do {
let parser = SwiftMarkdownParser()
let ast = try await parser.parseToAST(markdown)
// Check if task was cancelled
guard !Task.isCancelled else { return }
let renderer = SwiftUIRenderer()
let view = try await renderer.render(document: ast)
await MainActor.run {
self.content = view
}
} catch {
await MainActor.run {
self.content = AnyView(Text("Failed to render"))
}
}
}
}
}
Advanced Integration
SwiftUI App Integration
import SwiftUI
import SwiftMarkdownParser
@main
struct MarkdownApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@StateObject private var documentStore = DocumentStore()
var body: some View {
NavigationSplitView {
DocumentList(documents: documentStore.documents)
} detail: { document in
DocumentDetailView(document: document)
}
}
}
struct DocumentDetailView: View {
let document: MarkdownDocument
@Environment(\.colorScheme) var colorScheme
private var styleConfig: SwiftUIStyleConfiguration {
SwiftUIStyleConfiguration(
bodyFont: .body,
linkColor: colorScheme == .dark ? .blue : .blue,
codeBackgroundColor: Color(.systemGray6)
)
}
var body: some View {
ScrollView {
MarkdownView(
markdown: document.content,
styleConfiguration: styleConfig
)
.padding()
}
.navigationTitle(document.title)
.navigationBarTitleDisplayMode(.large)
}
}
Integration with Navigation
struct NavigableMarkdownView: View {
let markdown: String
@State private var navigationPath = NavigationPath()
var body: some View {
NavigationStack(path: $navigationPath) {
MarkdownView(
markdown: markdown,
context: SwiftUIRenderContext(
linkHandler: { url in
handleNavigation(url: url)
}
)
)
.navigationDestination(for: String.self) { destination in
DetailView(content: destination)
}
}
}
private func handleNavigation(url: URL) {
if url.scheme == "internal" {
navigationPath.append(url.absoluteString)
} else {
UIApplication.shared.open(url)
}
}
}
Custom View Wrappers
struct StyledMarkdownView: View {
let markdown: String
let theme: MarkdownTheme
var body: some View {
MarkdownView(
markdown: markdown,
styleConfiguration: theme.styleConfiguration
)
.background(theme.backgroundColor)
.cornerRadius(theme.cornerRadius)
.shadow(radius: theme.shadowRadius)
}
}
struct MarkdownTheme {
let styleConfiguration: SwiftUIStyleConfiguration
let backgroundColor: Color
let cornerRadius: CGFloat
let shadowRadius: CGFloat
static let paper = MarkdownTheme(
styleConfiguration: SwiftUIStyleConfiguration(
bodyFont: .system(.body, design: .serif),
textColor: Color(.label),
codeBackgroundColor: Color(.systemGray6)
),
backgroundColor: Color(.systemBackground),
cornerRadius: 12,
shadowRadius: 2
)
static let code = MarkdownTheme(
styleConfiguration: SwiftUIStyleConfiguration(
bodyFont: .system(.body, design: .monospaced),
textColor: Color(.label),
codeBackgroundColor: Color(.systemGray5)
),
backgroundColor: Color(.systemGray6),
cornerRadius: 8,
shadowRadius: 1
)
}
Error Handling
Graceful Error Display
struct RobustMarkdownView: View {
let markdown: String
@State private var renderState: RenderState = .loading
enum RenderState {
case loading
case success(AnyView)
case failure(Error)
}
var body: some View {
Group {
switch renderState {
case .loading:
ProgressView("Rendering...")
case .success(let view):
view
case .failure(let error):
ErrorView(error: error) {
renderMarkdown()
}
}
}
.task {
await renderMarkdown()
}
}
private func renderMarkdown() async {
do {
let parser = SwiftMarkdownParser()
let ast = try await parser.parseToAST(markdown)
let renderer = SwiftUIRenderer()
let view = try await renderer.render(document: ast)
await MainActor.run {
self.renderState = .success(view)
}
} catch {
await MainActor.run {
self.renderState = .failure(error)
}
}
}
}
struct ErrorView: View {
let error: Error
let retry: () -> Void
var body: some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle")
.font(.largeTitle)
.foregroundColor(.orange)
Text("Rendering Failed")
.font(.headline)
Text(error.localizedDescription)
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
Button("Retry", action: retry)
.buttonStyle(.borderedProminent)
}
.padding()
}
}
Platform Differences
iOS-Specific Features
#if os(iOS)
struct iOSMarkdownView: View {
let markdown: String
var body: some View {
MarkdownView(
markdown: markdown,
context: SwiftUIRenderContext(
linkHandler: { url in
// iOS-specific link handling
let safariVC = SFSafariViewController(url: url)
UIApplication.shared.windows.first?.rootViewController?.present(safariVC, animated: true)
}
)
)
.refreshable {
// Pull to refresh on iOS
await refreshContent()
}
}
}
#endif
macOS-Specific Features
#if os(macOS)
struct macOSMarkdownView: View {
let markdown: String
var body: some View {
MarkdownView(
markdown: markdown,
context: SwiftUIRenderContext(
linkHandler: { url in
// macOS-specific link handling
NSWorkspace.shared.open(url)
}
)
)
.frame(maxWidth: 800) // Optimal reading width
.background(Color(.windowBackgroundColor))
}
}
#endif
API Reference
SwiftUIRenderer
@available(iOS 17.0, macOS 14.0, *)
public struct SwiftUIRenderer: MarkdownRenderer {
public typealias Output = AnyView
public let context: SwiftUIRenderContext
public init(context: SwiftUIRenderContext = SwiftUIRenderContext())
public func render(document: AST.DocumentNode) async throws -> AnyView
public func render(node: ASTNode) async throws -> AnyView
}
SwiftUIRenderContext
@available(iOS 17.0, macOS 14.0, *)
public struct SwiftUIRenderContext: Sendable {
public let baseURL: URL?
public let styleConfiguration: SwiftUIStyleConfiguration
public let linkHandler: (@Sendable (URL) -> Void)?
public let imageHandler: (@Sendable (URL) -> AnyView)?
public let maxDepth: Int
public let enableAccessibility: Bool
public init(
baseURL: URL? = nil,
styleConfiguration: SwiftUIStyleConfiguration = SwiftUIStyleConfiguration(),
linkHandler: (@Sendable (URL) -> Void)? = nil,
imageHandler: (@Sendable (URL) -> AnyView)? = nil,
maxDepth: Int = 50,
enableAccessibility: Bool = true
)
}
SwiftUIStyleConfiguration
@available(iOS 17.0, macOS 14.0, *)
public struct SwiftUIStyleConfiguration: Sendable {
// Typography
public let bodyFont: Font
public let codeFont: Font
public let headingFonts: [Int: Font]
// Colors
public let textColor: Color
public let headingColor: Color
public let linkColor: Color
public let codeTextColor: Color
public let codeBackgroundColor: Color
// Spacing
public let documentSpacing: CGFloat
public let paragraphSpacing: CGFloat
public let listItemSpacing: CGFloat
// And many more styling properties...
public init(
bodyFont: Font = .body,
codeFont: Font = .system(.body, design: .monospaced),
headingFonts: [Int: Font] = defaultHeadingFonts,
textColor: Color = .primary,
// ... other parameters
)
}
Supported Elements
All CommonMark and GFM elements are fully supported:
- Text: Basic text, emphasis, strong emphasis, strikethrough
- Headings: All 6 levels with automatic font scaling
- Paragraphs: With proper spacing and alignment
- Lists: Ordered and unordered, with nesting support
- Code: Inline code spans and code blocks
- Links: Clickable links with custom handlers
- Images: AsyncImage integration with custom loading
- Tables: Native SwiftUI table layouts
- Task Lists: Checkboxes with custom styling
- Block Quotes: Styled containers with borders
- Thematic Breaks: Dividers and separators