Skip to content

Markdown in Apple's Text System

Use when the user is working with Markdown in SwiftUI Text or AttributedString and needs to know what renders, what is ignored, how PresentationIntent behaves, or when native Markdown stops being enough. Reach for this when the problem is Markdown semantics, not general attributed-string choice.

Reference Skills

Use when the user is working with Markdown in SwiftUI Text or AttributedString and needs to know what renders, what is ignored, how PresentationIntent behaves, or when native Markdown stops being enough. Reach for this when the problem is Markdown semantics, not general attributed-string choice.

Family: Rich Text And Formatting

Use this skill when the main question is how Markdown maps into Apple text APIs and where rendering gaps remain.

  • You are parsing or rendering Markdown in Text, UITextView, or TextKit.
  • You need PresentationIntent behavior explained.
  • You are deciding between native Markdown support and a third-party renderer.
  • Simple inline Markdown in SwiftUI Text -> native support is fine
  • Full Markdown document rendering or editing -> stay here
  • Regex/parsing mechanics are the main question -> /skill apple-text-parsing
// String literals in Text are LocalizedStringKey — Markdown processed automatically
Text("**Bold** and *italic* and `code`")
Text("~~Strikethrough~~ and [Link](https://apple.com)")
Text("***Bold italic*** together")
SyntaxRenders?Result
**bold** / __bold__Bold text
*italic* / _italic_Italic text
***bold italic***Bold + italic
`code`Monospaced font
~~strikethrough~~Strikethrough
[text](url)Tappable link (accent color)
SyntaxRenders?What Happens
# HeadingTreated as plain text or ignored
- Item / 1. ItemNot rendered as list
> QuoteNot rendered as quote
```code block```Not rendered as code block
![alt](image)Images not supported
TablesNot supported
--- (horizontal rule)Not supported
Task lists - [ ]Not supported

Only inline Markdown works in SwiftUI Text. Block-level Markdown is not rendered.

// ✅ Markdown renders — literal is LocalizedStringKey
Text("**bold** text")
// ❌ Markdown does NOT render — String variable
let text: String = "**bold** text"
Text(text) // Displays literal asterisks
// ✅ Force Markdown on String variable
let text: String = "**bold** text"
Text(LocalizedStringKey(text))
// ❌ Disable Markdown
Text(verbatim: "**not bold**") // Displays literal asterisks
// Inline-only parsing (safe for user content)
let str = try AttributedString(
markdown: "**Bold** and [link](https://apple.com)",
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
)
Text(str)
OptionParsesWhitespaceBest For
.inlineOnlyInline only (bold, italic, code, links, strikethrough)CollapsesSimple formatted text
.inlineOnlyPreservingWhitespaceInline onlyPreserves originalChat messages, user input
.fullALL Markdown (inline + block-level)Markdown rulesDocuments, articles
let markdown = """
# Heading
- Item 1
- Item 2
> A quote

code block

"""
let str = try AttributedString(
markdown: markdown,
options: .init(interpretedSyntax: .full)
)
// Block-level structure stored in presentationIntent attribute
for run in str.runs {
if let intent = run.presentationIntent {
for component in intent.components {
switch component.kind {
case .header(let level):
print("Heading level \(level)")
case .unorderedList:
print("Unordered list")
case .listItem(let ordinal):
print("List item \(ordinal)")
case .blockQuote:
print("Block quote")
case .codeBlock(let languageHint):
print("Code block: \(languageHint ?? "none")")
case .paragraph:
print("Paragraph")
case .orderedList:
print("Ordered list")
case .table:
print("Table")
case .tableHeaderRow:
print("Table header")
case .tableRow(let index):
print("Table row \(index)")
case .tableCell(let column):
print("Table cell column \(column)")
case .thematicBreak:
print("Horizontal rule")
@unknown default:
break
}
}
}
}

Critical: SwiftUI Text ignores presentationIntent entirely. It stores the data but renders nothing differently for headings, lists, quotes, etc.

Rendering PresentationIntent in UITextView

Section titled “Rendering PresentationIntent in UITextView”

To actually render block-level Markdown in TextKit, you must interpret PresentationIntent and apply paragraph styles:

func applyBlockFormatting(to attrStr: AttributedString) -> NSAttributedString {
let mutable = NSMutableAttributedString(attrStr)
for run in attrStr.runs {
guard let intent = run.presentationIntent else { continue }
let nsRange = NSRange(run.range, in: attrStr)
let style = NSMutableParagraphStyle()
for component in intent.components {
switch component.kind {
case .header(let level):
let sizes: [Int: CGFloat] = [1: 28, 2: 24, 3: 20, 4: 18, 5: 16, 6: 14]
let fontSize = sizes[level] ?? 16
mutable.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: fontSize), range: nsRange)
style.paragraphSpacingBefore = 12
style.paragraphSpacing = 8
case .unorderedList, .orderedList:
style.headIndent = 24
style.firstLineHeadIndent = 8
case .blockQuote:
style.headIndent = 16
style.firstLineHeadIndent = 16
mutable.addAttribute(.foregroundColor, value: UIColor.secondaryLabel, range: nsRange)
case .codeBlock:
mutable.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: 14, weight: .regular), range: nsRange)
mutable.addAttribute(.backgroundColor, value: UIColor.secondarySystemBackground, range: nsRange)
default: break
}
}
mutable.addAttribute(.paragraphStyle, value: style, range: nsRange)
}
return mutable
}

Custom attributes use ^[text](key: value) in Markdown:

This has ^[custom styling](highlight: true, color: 'blue')
// Step 1: Define attribute key
enum HighlightAttribute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {
typealias Value = Bool
static let name = "highlight"
}
enum ColorNameAttribute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {
typealias Value = String
static let name = "color"
}
// Step 2: Create scope
extension AttributeScopes {
struct MyMarkdownAttributes: AttributeScope {
let highlight: HighlightAttribute
let color: ColorNameAttribute
let foundation: FoundationAttributes
let swiftUI: SwiftUIAttributes
}
var myMarkdown: MyMarkdownAttributes.Type { MyMarkdownAttributes.self }
}
// Step 3: Dynamic lookup
extension AttributeDynamicLookup {
subscript<T: AttributedStringKey>(
dynamicMember keyPath: KeyPath<AttributeScopes.MyMarkdownAttributes, T>
) -> T { self[T.self] }
}
// Step 4: Parse
let str = try AttributedString(
markdown: "This is ^[highlighted](highlight: true, color: 'blue') text",
including: \.myMarkdown
)
// Step 5: Use
for run in str.runs {
if run.highlight == true {
print("Color: \(run.color ?? "default")")
}
}

Pros:

  • No dependencies
  • Codable for serialization
  • Custom attributes via MarkdownDecodableAttributedStringKey
  • Type-safe
  • Localization-aware

Cons:

  • SwiftUI Text renders ONLY inline formatting
  • Block-level requires manual PresentationIntent interpretation
  • No image support
  • No table rendering
  • Significant work for full document rendering

Third-Party: MarkdownUI (gonzalezreal/swift-markdown-ui)

Section titled “Third-Party: MarkdownUI (gonzalezreal/swift-markdown-ui)”

Pros:

  • Renders full Markdown in SwiftUI (headings, lists, code blocks, images, tables, block quotes)
  • Themeable
  • Syntax highlighting for code blocks
  • No manual PresentationIntent work

Cons:

  • External dependency
  • May not match your exact design needs
  • Custom rendering harder than native Text
  • Additional maintenance burden

Pros:

  • Official Apple package
  • Full CommonMark parser
  • AST access for custom rendering
  • Used by DocC

Cons:

  • Parser only — no views included
  • Must build your own rendering pipeline
NeedSolution
Simple bold/italic/links in SwiftUINative Text("**bold**")
Full Markdown document in SwiftUIMarkdownUI library
Markdown in UITextViewParse with .full, interpret PresentationIntent
Custom Markdown attributesMarkdownDecodableAttributedStringKey
Markdown AST manipulationswift-markdown (Apple)
Editable MarkdownTextKit view with syntax highlighting
  1. Expecting Text to render headings/lists — SwiftUI Text only renders inline Markdown. Block-level is silently ignored.
  2. String variable doesn’t render Markdown — Only LocalizedStringKey triggers Markdown. Wrap: Text(LocalizedStringKey(string)).
  3. .full parsing without PresentationIntent handling — You get the data but nothing renders differently unless you interpret it.
  4. Forgetting custom scope in parsingAttributedString(markdown:) without including: ignores custom ^[](key: value) attributes.
  5. Assuming PresentationIntent = visual rendering — It’s structural data, not rendering instructions. You must map it to visual attributes yourself.

This page documents the apple-text-markdown reference skill. Use it when the subsystem is already known and you need mechanics, behavior, or API detail.

  • apple-text-attributed-string: Use when choosing between AttributedString and NSAttributedString, defining custom attributes, converting between them, or deciding which model should own rich text in a feature. Reach for this when the main task is the attributed-string model decision, not low-level formatting catalog lookup.
  • apple-text-swiftui-bridging: Use when deciding whether a text type or attribute model crosses the SwiftUI and TextKit boundary cleanly, such as AttributedString, NSAttributedString, UITextView, or SwiftUI Text. Reach for this when the main question is interoperability and support boundaries, not wrapper mechanics.
  • apple-text-writing-tools: Use when integrating Writing Tools into a native or custom text editor, configuring writingToolsBehavior, adopting UIWritingToolsCoordinator, protecting ranges, or debugging why Writing Tools do not appear. Reach for this when the problem is specifically Writing Tools, not generic editor debugging.
Full SKILL.md source
SKILL.md
---
name: apple-text-markdown
description: Use when the user is working with Markdown in SwiftUI Text or AttributedString and needs to know what renders, what is ignored, how PresentationIntent behaves, or when native Markdown stops being enough. Reach for this when the problem is Markdown semantics, not general attributed-string choice.
license: MIT
---
# Markdown in Apple's Text System
Use this skill when the main question is how Markdown maps into Apple text APIs and where rendering gaps remain.
## When to Use
- You are parsing or rendering Markdown in `Text`, `UITextView`, or TextKit.
- You need `PresentationIntent` behavior explained.
- You are deciding between native Markdown support and a third-party renderer.
## Quick Decision
- Simple inline Markdown in SwiftUI `Text` -> native support is fine
- Full Markdown document rendering or editing -> stay here
- Regex/parsing mechanics are the main question -> `/skill apple-text-parsing`
## Core Guidance
## SwiftUI Text Markdown (iOS 15+)
### What Renders Automatically
```swift
// String literals in Text are LocalizedStringKey — Markdown processed automatically
Text("**Bold** and *italic* and `code`")
Text("~~Strikethrough~~ and [Link](https://apple.com)")
Text("***Bold italic*** together")
```
| Syntax | Renders? | Result |
|--------|----------|--------|
| `**bold**` / `__bold__` | ✅ | Bold text |
| `*italic*` / `_italic_` | ✅ | Italic text |
| `***bold italic***` | ✅ | Bold + italic |
| `` `code` `` | ✅ | Monospaced font |
| `~~strikethrough~~` | ✅ | Strikethrough |
| `[text](url)` | ✅ | Tappable link (accent color) |
### What Does NOT Render
| Syntax | Renders? | What Happens |
|--------|----------|-------------|
| `# Heading` | ❌ | Treated as plain text or ignored |
| `- Item` / `1. Item` | ❌ | Not rendered as list |
| `> Quote` | ❌ | Not rendered as quote |
| ```` ```code block``` ```` | ❌ | Not rendered as code block |
| `![alt](image)` | ❌ | Images not supported |
| Tables | ❌ | Not supported |
| `---` (horizontal rule) | ❌ | Not supported |
| Task lists `- [ ]` | ❌ | Not supported |
**Only inline Markdown works in SwiftUI Text.** Block-level Markdown is not rendered.
### String vs LocalizedStringKey
```swift
// ✅ Markdown renders — literal is LocalizedStringKey
Text("**bold** text")
// ❌ Markdown does NOT render — String variable
let text: String = "**bold** text"
Text(text) // Displays literal asterisks
// ✅ Force Markdown on String variable
let text: String = "**bold** text"
Text(LocalizedStringKey(text))
// ❌ Disable Markdown
Text(verbatim: "**not bold**") // Displays literal asterisks
```
### With AttributedString
```swift
// Inline-only parsing (safe for user content)
let str = try AttributedString(
markdown: "**Bold** and [link](https://apple.com)",
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
)
Text(str)
```
## AttributedString Markdown Parsing
### interpretedSyntax Options
| Option | Parses | Whitespace | Best For |
|--------|--------|-----------|----------|
| `.inlineOnly` | Inline only (bold, italic, code, links, strikethrough) | Collapses | Simple formatted text |
| `.inlineOnlyPreservingWhitespace` | Inline only | Preserves original | Chat messages, user input |
| `.full` | ALL Markdown (inline + block-level) | Markdown rules | Documents, articles |
### Block-Level Parsing with `.full`
```swift
let markdown = """
# Heading
- Item 1
- Item 2
> A quote
```
code block
```
"""
let str = try AttributedString(
markdown: markdown,
options: .init(interpretedSyntax: .full)
)
// Block-level structure stored in presentationIntent attribute
for run in str.runs {
if let intent = run.presentationIntent {
for component in intent.components {
switch component.kind {
case .header(let level):
print("Heading level \(level)")
case .unorderedList:
print("Unordered list")
case .listItem(let ordinal):
print("List item \(ordinal)")
case .blockQuote:
print("Block quote")
case .codeBlock(let languageHint):
print("Code block: \(languageHint ?? "none")")
case .paragraph:
print("Paragraph")
case .orderedList:
print("Ordered list")
case .table:
print("Table")
case .tableHeaderRow:
print("Table header")
case .tableRow(let index):
print("Table row \(index)")
case .tableCell(let column):
print("Table cell column \(column)")
case .thematicBreak:
print("Horizontal rule")
@unknown default:
break
}
}
}
}
```
**Critical:** SwiftUI Text ignores `presentationIntent` entirely. It stores the data but renders nothing differently for headings, lists, quotes, etc.
### Rendering PresentationIntent in UITextView
To actually render block-level Markdown in TextKit, you must interpret PresentationIntent and apply paragraph styles:
```swift
func applyBlockFormatting(to attrStr: AttributedString) -> NSAttributedString {
let mutable = NSMutableAttributedString(attrStr)
for run in attrStr.runs {
guard let intent = run.presentationIntent else { continue }
let nsRange = NSRange(run.range, in: attrStr)
let style = NSMutableParagraphStyle()
for component in intent.components {
switch component.kind {
case .header(let level):
let sizes: [Int: CGFloat] = [1: 28, 2: 24, 3: 20, 4: 18, 5: 16, 6: 14]
let fontSize = sizes[level] ?? 16
mutable.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: fontSize), range: nsRange)
style.paragraphSpacingBefore = 12
style.paragraphSpacing = 8
case .unorderedList, .orderedList:
style.headIndent = 24
style.firstLineHeadIndent = 8
case .blockQuote:
style.headIndent = 16
style.firstLineHeadIndent = 16
mutable.addAttribute(.foregroundColor, value: UIColor.secondaryLabel, range: nsRange)
case .codeBlock:
mutable.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: 14, weight: .regular), range: nsRange)
mutable.addAttribute(.backgroundColor, value: UIColor.secondarySystemBackground, range: nsRange)
default: break
}
}
mutable.addAttribute(.paragraphStyle, value: style, range: nsRange)
}
return mutable
}
```
## Custom Markdown Attributes
### Syntax
Custom attributes use `^[text](key: value)` in Markdown:
```markdown
This has ^[custom styling](highlight: true, color: 'blue')
```
### Implementation
```swift
// Step 1: Define attribute key
enum HighlightAttribute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {
typealias Value = Bool
static let name = "highlight"
}
enum ColorNameAttribute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {
typealias Value = String
static let name = "color"
}
// Step 2: Create scope
extension AttributeScopes {
struct MyMarkdownAttributes: AttributeScope {
let highlight: HighlightAttribute
let color: ColorNameAttribute
let foundation: FoundationAttributes
let swiftUI: SwiftUIAttributes
}
var myMarkdown: MyMarkdownAttributes.Type { MyMarkdownAttributes.self }
}
// Step 3: Dynamic lookup
extension AttributeDynamicLookup {
subscript<T: AttributedStringKey>(
dynamicMember keyPath: KeyPath<AttributeScopes.MyMarkdownAttributes, T>
) -> T { self[T.self] }
}
// Step 4: Parse
let str = try AttributedString(
markdown: "This is ^[highlighted](highlight: true, color: 'blue') text",
including: \.myMarkdown
)
// Step 5: Use
for run in str.runs {
if run.highlight == true {
print("Color: \(run.color ?? "default")")
}
}
```
## Native vs Third-Party Markdown
### Native (AttributedString + SwiftUI Text)
**Pros:**
- No dependencies
- Codable for serialization
- Custom attributes via `MarkdownDecodableAttributedStringKey`
- Type-safe
- Localization-aware
**Cons:**
- SwiftUI Text renders ONLY inline formatting
- Block-level requires manual PresentationIntent interpretation
- No image support
- No table rendering
- Significant work for full document rendering
### Third-Party: MarkdownUI (gonzalezreal/swift-markdown-ui)
**Pros:**
- Renders full Markdown in SwiftUI (headings, lists, code blocks, images, tables, block quotes)
- Themeable
- Syntax highlighting for code blocks
- No manual PresentationIntent work
**Cons:**
- External dependency
- May not match your exact design needs
- Custom rendering harder than native Text
- Additional maintenance burden
### Third-Party: Apple's swift-markdown
**Pros:**
- Official Apple package
- Full CommonMark parser
- AST access for custom rendering
- Used by DocC
**Cons:**
- Parser only — no views included
- Must build your own rendering pipeline
### Decision Guide
| Need | Solution |
|------|----------|
| Simple bold/italic/links in SwiftUI | Native `Text("**bold**")` |
| Full Markdown document in SwiftUI | MarkdownUI library |
| Markdown in UITextView | Parse with `.full`, interpret PresentationIntent |
| Custom Markdown attributes | `MarkdownDecodableAttributedStringKey` |
| Markdown AST manipulation | swift-markdown (Apple) |
| Editable Markdown | TextKit view with syntax highlighting |
## Common Pitfalls
1. **Expecting Text to render headings/lists** — SwiftUI Text only renders inline Markdown. Block-level is silently ignored.
2. **String variable doesn't render Markdown** — Only `LocalizedStringKey` triggers Markdown. Wrap: `Text(LocalizedStringKey(string))`.
3. **`.full` parsing without PresentationIntent handling** — You get the data but nothing renders differently unless you interpret it.
4. **Forgetting custom scope in parsing**`AttributedString(markdown:)` without `including:` ignores custom `^[](key: value)` attributes.
5. **Assuming PresentationIntent = visual rendering** — It's structural data, not rendering instructions. You must map it to visual attributes yourself.
## Related Skills
- Use `/skill apple-text-attributed-string` for attribute-model and conversion choices.
- Use `/skill apple-text-swiftui-bridging` when the real gap is SwiftUI rendering limits.
- Use `/skill apple-text-parsing` when the problem is parser choice more than Markdown rendering semantics.