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.
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.
When to Use
Section titled “When to Use”- You are parsing or rendering Markdown in
Text,UITextView, or TextKit. - You need
PresentationIntentbehavior explained. - You are deciding between native Markdown support and a third-party renderer.
Quick Decision
Section titled “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
Section titled “Core Guidance”SwiftUI Text Markdown (iOS 15+)
Section titled “SwiftUI Text Markdown (iOS 15+)”What Renders Automatically
Section titled “What Renders Automatically”// String literals in Text are LocalizedStringKey — Markdown processed automaticallyText("**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
Section titled “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 |
 | ❌ | 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
Section titled “String vs LocalizedStringKey”// ✅ Markdown renders — literal is LocalizedStringKeyText("**bold** text")
// ❌ Markdown does NOT render — String variablelet text: String = "**bold** text"Text(text) // Displays literal asterisks
// ✅ Force Markdown on String variablelet text: String = "**bold** text"Text(LocalizedStringKey(text))
// ❌ Disable MarkdownText(verbatim: "**not bold**") // Displays literal asterisksWith AttributedString
Section titled “With AttributedString”// 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
Section titled “AttributedString Markdown Parsing”interpretedSyntax Options
Section titled “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
Section titled “Block-Level Parsing with .full”let markdown = """# Heading
- Item 1- Item 2
> A quotecode block
"""
let str = try AttributedString( markdown: markdown, options: .init(interpretedSyntax: .full))
// Block-level structure stored in presentationIntent attributefor 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 Markdown Attributes
Section titled “Custom Markdown Attributes”Syntax
Section titled “Syntax”Custom attributes use ^[text](key: value) in Markdown:
This has ^[custom styling](highlight: true, color: 'blue')Implementation
Section titled “Implementation”// Step 1: Define attribute keyenum HighlightAttribute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey { typealias Value = Bool static let name = "highlight"}
enum ColorNameAttribute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey { typealias Value = String static let name = "color"}
// Step 2: Create scopeextension 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 lookupextension AttributeDynamicLookup { subscript<T: AttributedStringKey>( dynamicMember keyPath: KeyPath<AttributeScopes.MyMarkdownAttributes, T> ) -> T { self[T.self] }}
// Step 4: Parselet str = try AttributedString( markdown: "This is ^[highlighted](highlight: true, color: 'blue') text", including: \.myMarkdown)
// Step 5: Usefor run in str.runs { if run.highlight == true { print("Color: \(run.color ?? "default")") }}Native vs Third-Party Markdown
Section titled “Native vs Third-Party Markdown”Native (AttributedString + SwiftUI Text)
Section titled “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)
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
Third-Party: Apple’s swift-markdown
Section titled “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
Section titled “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
Section titled “Common Pitfalls”- Expecting Text to render headings/lists — SwiftUI Text only renders inline Markdown. Block-level is silently ignored.
- String variable doesn’t render Markdown — Only
LocalizedStringKeytriggers Markdown. Wrap:Text(LocalizedStringKey(string)). .fullparsing without PresentationIntent handling — You get the data but nothing renders differently unless you interpret it.- Forgetting custom scope in parsing —
AttributedString(markdown:)withoutincluding:ignores custom^[](key: value)attributes. - Assuming PresentationIntent = visual rendering — It’s structural data, not rendering instructions. You must map it to visual attributes yourself.
Documentation Scope
Section titled “Documentation Scope”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.
Related
Section titled “Related”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
---name: apple-text-markdowndescription: 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 automaticallyText("**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 || `` | ❌ | 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 LocalizedStringKeyText("**bold** text")
// ❌ Markdown does NOT render — String variablelet text: String = "**bold** text"Text(text) // Displays literal asterisks
// ✅ Force Markdown on String variablelet text: String = "**bold** text"Text(LocalizedStringKey(text))
// ❌ Disable MarkdownText(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`
```swiftlet 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 attributefor 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:
```swiftfunc 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:
```markdownThis has ^[custom styling](highlight: true, color: 'blue')```
### Implementation
```swift// Step 1: Define attribute keyenum HighlightAttribute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey { typealias Value = Bool static let name = "highlight"}
enum ColorNameAttribute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey { typealias Value = String static let name = "color"}
// Step 2: Create scopeextension 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 lookupextension AttributeDynamicLookup { subscript<T: AttributedStringKey>( dynamicMember keyPath: KeyPath<AttributeScopes.MyMarkdownAttributes, T> ) -> T { self[T.self] }}
// Step 4: Parselet str = try AttributedString( markdown: "This is ^[highlighted](highlight: true, color: 'blue') text", including: \.myMarkdown)
// Step 5: Usefor 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.