Displaying Markdown in SwiftUI: A Step-by-Step Tutorial
This article contains tips on how to easily display Markdown in SwiftUI using the Text widget, as well as how to change link colors and use AttributedString
Introduction to Markdown in SwiftUI
Since the groundbreaking release of SwiftUI for iOS 15, Apple introduced native built-in support for rendering Markdown text directly through the standard Text widget. This native integration drastically reduced the necessity for third-party parsing libraries when developers merely wanted to embed bold text, italics, strikethroughs, or hyperlinks.
To write inline Markdown within SwiftUI, you simply construct a standard Text view and write the Markdown payload right inside its initialization string parameter. The framework intelligently parses the syntax during layout compilation.
struct ContentView: View {
var body: some View {
VStack(spacing: 12) {
Text("This is just regular Text")
Text("This with **Bold** style")
Text("This with *Italic* style")
Text("This one with ~Strikethrough~ style")
Text("This one with `Monopace` style")
Text("This one with a [Go to Google](https://www.google.com) Link")
}
.padding()
}
}
Writing Markdown in a Variable
As your content grows or gets populated from an external API, you'll naturally want to source your Markdown from an isolated variable instead of hard-coding literals in the view body. A beginner might approach it like this:
struct ContentView: View {
@State private var text: String = "**SwiftUI** helps you build great-looking apps across all _Apple_ platforms with the power of Swift — and surprisingly little code. [Learn SwiftUI](https://developer.apple.com/xcode/swiftui/)"
var body: some View {
VStack {
Text(text) // This won't parse the markdown!
}
.padding()
}
}Unfortunately, this straightforward approach won't work as expected. Instead of rendering the stylized Markdown, SwiftUI renders raw characters verbatim, exposing asterisks and brackets natively.

Why does this fail? Under the hood, if you write a string literal inside Text("..."), the Swift compiler treats it as a LocalizedStringKey natively, automatically triggering the Markdown parsing pipeline. Conversely, passing a pure generic String variable forces standard unformatted textual representation.
Therefore, to bind a populated variable properly to Markdown styling, you must either explicitly annotate your property as a LocalizedStringKey or cast it ad-hoc before injection.
struct ContentView: View {
@State private var rawString: String = "Hello **World**"
// Approach 1: Declare explicitly as LocalizedStringKey
@State private var localizeText: LocalizedStringKey = "**SwiftUI** helps you build great-looking apps... [SwiftUI](https://developer.apple.com/xcode/swiftui/)"
var body: some View {
VStack(spacing: 20) {
// Approach 2: Force casting Strings
Text(LocalizedStringKey(rawString))
Divider()
// Using predefined LocalizedStringKey
Text(localizeText)
}
.padding()
}
}
Changing Link Color
By default, any inline Markdown link inherently adopts Apple's global interactive accent (which defaults to standard royal blue on most devices). While that guarantees a consistent OS-level feel, branding might demand otherwise.
To easily mutate the hyperlinked segments within your text block, append the .tint() modifier. This ensures the tappable boundaries are repainted natively without tearing down the underlying localized key setup.
struct ContentView: View {
var body: some View {
VStack(spacing: 12) {
Text("Regular [link](https://apple.com) with default color")
Text("Styled [link](https://apple.com) with a custom [hyperlink](https://apple.com) color")
.tint(.pink) // Overriding default blue
}
.padding()
}
}
Working with AttributedString
For complex payload handling where you need varying fonts, background highlights, and inline spacing, simple Markdown falls short. This is where AttributedString excels. Introduced in recent iOS iterations, AttributedString is a modern, value-type replacement for the older Objective-C NSAttributedString.
By constructing an AttributedString from your Markdown payload, you unlock profound programmatic access to specific text ranges and ranges of stylized characters.
struct ContentView: View {
private var attrText: AttributedString {
do {
var text = try AttributedString(
markdown: "**SwiftUI** helps you build great-looking apps across all _[Apple](https://apple.com)_ platforms with the power of Swift — and surprisingly little code."
)
// Highlighting a specific substring
if let range = text.range(of: "SwiftUI") {
text[range].backgroundColor = .yellow
text[range].foregroundColor = .red
}
// Exaggerating font scaling
if let range = text.range(of: "Apple") {
text[range].foregroundColor = .purple
text[range].font = .system(size: 30)
}
return text
} catch {
return "Failed to parse markdown payload."
}
}
var body: some View {
VStack {
Text(attrText)
}
.padding()
}
}
Markdown Limitations and Workarounds
While SwiftUI makes rendering text layouts a breeze, pure inline Markdown formatting explicitly lacks support for block-level elements currently natively bound to web rendering engines. Specifically, SwiftUI's Text does not support:
1. Embedded images - Markdown tags like `` will not automatically pull assets into SwiftUI components. 2. Numbered lists or Bullet points - It won't structurally auto-indent bullet parameters. 3. Tables and Headings - While you can artificially inflate font weights manually, `### Heading` gets parsed as standard plaintext.
If your application heavily relies on CMS delivery with comprehensive Markdown trees, you must utilize highly specialized Swift packages (such as `MarkdownUI`) or leverage raw UITextView abstractions bridging over to UIKit.
Conclusion
Displaying Markdown in SwiftUI is surprisingly simple yet incredibly malleable. It serves brilliantly for simple formatting inside localized assets or fetching small database blurbs. As soon as you outgrow its native simplicity, casting via AttributedString guarantees endless micro-typography adjustments for an aesthetic finish.