apple-text-markdown
Use when working with Markdown in SwiftUI Text or AttributedString — what renders, PresentationIntent, or rendering gaps
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
PresentationIntentbehavior 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
// 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 |
 | ❌ | 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
// ✅ 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
// 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
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:
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:
This has ^[custom styling](highlight: true, color: 'blue')
Implementation
// 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
- 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.
Related Skills
- Use
/skill apple-text-attributed-stringfor attribute-model and conversion choices. - Use
/skill apple-text-swiftui-bridgingwhen the real gap is SwiftUI rendering limits. - Use
/skill apple-text-parsingwhen the problem is parser choice more than Markdown rendering semantics.