text-rich-content
SwiftUI Text and Rich Content
Comprehensive guide to SwiftUI text rendering, AttributedString, native Markdown support, and rich text editing for iOS 26 development.
Prerequisites
- iOS 15+ for AttributedString (iOS 26 recommended for rich text editing)
- Xcode 26+
Basic Text
Text View Fundamentals
// Simple text
Text("Hello, World!")
// Multi-line text (automatic)
Text("This is a longer piece of text that will automatically wrap to multiple lines when it exceeds the available width.")
// Verbatim (no localization)
Text(verbatim: "user_name") // Won't look up in Localizable.strings
Font Modifiers
Text("Hello")
.font(.largeTitle)
.font(.title)
.font(.title2)
.font(.title3)
.font(.headline)
.font(.subheadline)
.font(.body)
.font(.callout)
.font(.caption)
.font(.caption2)
.font(.footnote)
// Custom font
Text("Custom")
.font(.custom("Helvetica Neue", size: 24))
.font(.system(size: 20, weight: .bold, design: .rounded))
// Dynamic type with relative size
Text("Scaled")
.font(.body.leading(.loose))
Text Styling
Text("Styled Text")
.fontWeight(.bold)
.italic()
.underline()
.underline(color: .blue)
.strikethrough()
.strikethrough(color: .red)
.kerning(2) // Letter spacing
.tracking(2) // Similar to kerning
.baselineOffset(10) // Vertical offset
.textCase(.uppercase)
.textCase(.lowercase)
Text Truncation and Lines
Text("Long text that might need truncation...")
.lineLimit(2)
.lineLimit(1...3) // Range (iOS 16+)
.truncationMode(.tail) // .head, .middle, .tail
.allowsTightening(true) // Reduce spacing before truncating
.minimumScaleFactor(0.5) // Scale down to fit
Text Alignment
Text("Aligned text")
.multilineTextAlignment(.leading)
.multilineTextAlignment(.center)
.multilineTextAlignment(.trailing)
// Frame alignment for single line
Text("Single")
.frame(maxWidth: .infinity, alignment: .leading)
Native Markdown Support
Automatic Markdown Rendering
SwiftUI Text views automatically render Markdown:
// Basic Markdown in Text
Text("**Bold**, *italic*, and ~~strikethrough~~")
Text("Visit [Apple](https://apple.com)")
Text("`inline code` looks different")
// Combined formatting
Text("This is **bold and *italic* together**")
Supported Markdown Syntax
// Emphasis
Text("*italic* or _italic_")
Text("**bold** or __bold__")
Text("***bold italic***")
// Strikethrough
Text("~~deleted~~")
// Code
Text("`monospace`")
// Links
Text("[Link Text](https://example.com)")
// Soft breaks
Text("Line one\nLine two")
Markdown from Variables
// String interpolation with AttributedString
let markdownString = "**Important:** Check the [documentation](https://docs.example.com)"
// Option 1: Direct (for literals only)
Text("**Bold** text")
// Option 2: AttributedString for variables
if let attributed = try? AttributedString(markdown: markdownString) {
Text(attributed)
}
AttributedString
Creating AttributedString
// From plain string
var attributed = AttributedString("Hello World")
// From Markdown
let markdown = try? AttributedString(markdown: "**Bold** and *italic*")
// From localized string
let localized = AttributedString(localized: "greeting_message")
Applying Attributes
var text = AttributedString("Hello World")
// Whole string attributes
text.font = .title
text.foregroundColor = .blue
text.backgroundColor = .yellow
// Range-based attributes
if let range = text.range(of: "World") {
text[range].font = .title.bold()
text[range].foregroundColor = .red
}
Available Attributes
var text = AttributedString("Styled")
// Typography
text.font = .body
text.foregroundColor = .primary
text.backgroundColor = .clear
// Text decoration
text.strikethroughStyle = .single
text.strikethroughColor = .red
text.underlineStyle = .single
text.underlineColor = .blue
// Spacing
text.kern = 2.0 // Character spacing
text.tracking = 1.0 // Similar to kern
text.baselineOffset = 5 // Vertical offset
// Links
text.link = URL(string: "https://apple.com")
// Accessibility
text.accessibilityLabel = "Custom label"
text.accessibilitySpeechSpellsOutCharacters = true
Combining AttributedStrings
var greeting = AttributedString("Hello ")
greeting.font = .title
var name = AttributedString("World")
name.font = .title.bold()
name.foregroundColor = .blue
let combined = greeting + name
Text(combined)
Iterating Over Runs
let attributed = try? AttributedString(markdown: "**Bold** and *italic*")
// Iterate through styled runs
for run in attributed?.runs ?? [] {
print("Text: \(attributed?[run.range] ?? "")")
print("Font: \(run.font ?? .body)")
}
Markdown Parsing Options
Basic Parsing
let source = "# Heading\n**Bold** text"
// Default parsing
let attributed = try? AttributedString(markdown: source)
// With options
let options = AttributedString.MarkdownParsingOptions(
interpretedSyntax: .inlineOnlyPreservingWhitespace
)
let parsed = try? AttributedString(markdown: source, options: options)
Interpreted Syntax Options
// Full Markdown (default)
.interpretedSyntax: .full
// Inline only (no block elements)
.interpretedSyntax: .inlineOnly
// Inline, preserving whitespace
.interpretedSyntax: .inlineOnlyPreservingWhitespace
Handling Parse Errors
do {
let attributed = try AttributedString(markdown: source)
// Use attributed string
} catch {
// Fallback to plain text
let plain = AttributedString(source)
}
Custom Attribute Scopes
// Define custom attributes
enum MyAttributes: AttributeScope {
let customHighlight: CustomHighlightAttribute
}
struct CustomHighlightAttribute: CodableAttributedStringKey {
typealias Value = Bool
static let name = "customHighlight"
}
// Extend AttributeScopes
extension AttributeScopes {
var myAttributes: MyAttributes.Type { MyAttributes.self }
}
// Use custom attributes
var text = AttributedString("Highlighted")
text.customHighlight = true
Rich Text Editing (iOS 26)
TextEditor with AttributedString
iOS 26 introduces first-class rich text editing:
struct RichTextEditor: View {
@State private var content = AttributedString("Edit me with **formatting**")
@State private var selection = AttributedTextSelection()
var body: some View {
TextEditor(text: $content, selection: $selection)
.textEditorStyle(.plain)
}
}
AttributedTextSelection
struct FormattingEditor: View {
@State private var content = AttributedString()
@State private var selection = AttributedTextSelection()
var body: some View {
VStack {
// Formatting toolbar
HStack {
Button("Bold") { toggleBold() }
Button("Italic") { toggleItalic() }
Button("Underline") { toggleUnderline() }
}
TextEditor(text: $content, selection: $selection)
}
}
func toggleBold() {
content.transformAttributes(in: selection.range) { container in
// Toggle bold
if container.font?.isBold == true {
container.font = container.font?.removingBold()
} else {
container.font = container.font?.bold()
}
}
}
func toggleItalic() {
content.transformAttributes(in: selection.range) { container in
if container.font?.isItalic == true {
container.font = container.font?.removingItalic()
} else {
container.font = container.font?.italic()
}
}
}
func toggleUnderline() {
content.transformAttributes(in: selection.range) { container in
if container.underlineStyle != nil {
container.underlineStyle = nil
} else {
container.underlineStyle = .single
}
}
}
}
Built-in Keyboard Shortcuts
iOS 26 TextEditor supports standard keyboard shortcuts:
- ⌘B - Bold
- ⌘I - Italic
- ⌘U - Underline
Font Resolution Context
TextEditor(text: $content, selection: $selection)
.environment(\.fontResolutionContext, FontResolutionContext(
defaultFont: .body,
defaultForegroundColor: .primary
))
Text Interpolation
Format Styles
// Numbers
Text("Count: \(count)")
Text("Price: \(price, format: .currency(code: "USD"))")
Text("Percentage: \(value, format: .percent)")
Text("Decimal: \(number, format: .number.precision(.fractionLength(2)))")
// Dates
Text("Date: \(date, format: .dateTime)")
Text("Day: \(date, format: .dateTime.day().month().year())")
Text("Time: \(date, format: .dateTime.hour().minute())")
// Relative dates
Text(date, style: .relative) // "2 hours ago"
Text(date, style: .timer) // "2:30:00"
Text(date, style: .date) // "June 15, 2025"
Text(date, style: .time) // "3:30 PM"
Text(date, style: .offset) // "+2 hours"
// Date ranges
Text(startDate...endDate)
// Lists
Text(names, format: .list(type: .and)) // "Alice, Bob, and Charlie"
// Measurements
Text(distance, format: .measurement(width: .abbreviated))
Person Name Components
let name = PersonNameComponents(givenName: "John", familyName: "Doe")
Text(name, format: .name(style: .long))
ByteCount
Text(fileSize, format: .byteCount(style: .file))
Localization
LocalizedStringKey
// Automatic localization lookup
Text("welcome_message") // Looks up in Localizable.strings
// With interpolation
Text("greeting_\(username)") // "greeting_%@" in strings file
// Explicit localized string
Text(LocalizedStringKey("settings_title"))
String Catalogs (.xcstrings)
Modern localization uses String Catalogs:
// In String Catalog (Localizable.xcstrings)
// Key: "items_count"
// English: "%lld items"
// French: "%lld éléments"
Text("items_count \(count)")
Pluralization
// In String Catalog, define variants:
// "items_count" with plural variants:
// - zero: "No items"
// - one: "1 item"
// - other: "%lld items"
Text("items_count \(count)")
AttributedString Localization
// Localized with attributes
let attributed = AttributedString(localized: "formatted_message")
Text(attributed)
Text Selection
Enabling Selection
Text("Selectable text that users can copy")
.textSelection(.enabled)
// Disable selection
Text("Not selectable")
.textSelection(.disabled)
Selection on Lists
List(items) { item in
Text(item.content)
.textSelection(.enabled)
}
TextField and SecureField
Basic TextField
@State private var text = ""
TextField("Placeholder", text: $text)
// With prompt
TextField("Username", text: $username, prompt: Text("Enter username"))
// Axis for multiline
TextField("Description", text: $description, axis: .vertical)
.lineLimit(3...6)
TextField Styles
TextField("Input", text: $text)
.textFieldStyle(.automatic)
.textFieldStyle(.plain)
.textFieldStyle(.roundedBorder)
SecureField
SecureField("Password", text: $password)
Formatting TextField
// Number input
TextField("Amount", value: $amount, format: .currency(code: "USD"))
// Date input
TextField("Date", value: $date, format: .dateTime)
// Custom format
TextField("Phone", value: $phone, format: PhoneNumberFormat())
TextField Focus
@FocusState private var isFocused: Bool
TextField("Input", text: $text)
.focused($isFocused)
Button("Focus") {
isFocused = true
}
Keyboard Types
TextField("Email", text: $email)
.keyboardType(.emailAddress)
.textContentType(.emailAddress)
.autocapitalization(.none)
.autocorrectionDisabled()
TextField("Phone", text: $phone)
.keyboardType(.phonePad)
.textContentType(.telephoneNumber)
TextField("URL", text: $url)
.keyboardType(.URL)
.textContentType(.URL)
Submit Actions
TextField("Search", text: $query)
.onSubmit {
performSearch()
}
.submitLabel(.search)
// Submit labels: .done, .go, .join, .next, .return, .search, .send
Label
Basic Label
Label("Settings", systemImage: "gear")
Label("Document", image: "doc-icon")
// Custom label
Label {
Text("Custom")
.font(.headline)
} icon: {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
}
Label Styles
Label("Title", systemImage: "star")
.labelStyle(.automatic)
.labelStyle(.titleOnly)
.labelStyle(.iconOnly)
.labelStyle(.titleAndIcon)
Link
Basic Links
Link("Apple", destination: URL(string: "https://apple.com")!)
Link(destination: URL(string: "https://apple.com")!) {
Label("Visit Apple", systemImage: "safari")
}
Links in Text
// Using Markdown
Text("Visit [our website](https://example.com) for more info")
// Using AttributedString
var text = AttributedString("Visit our website")
if let range = text.range(of: "our website") {
text[range].link = URL(string: "https://example.com")
text[range].foregroundColor = .blue
}
Text(text)
Privacy Sensitive Content
Redaction
Text(sensitiveData)
.privacySensitive()
// Manual redaction
Text("Hidden Content")
.redacted(reason: .privacy)
.redacted(reason: .placeholder)
// Unredacted
Text("Always Visible")
.unredacted()
Conditional Redaction
struct ContentView: View {
@Environment(\.redactionReasons) var redactionReasons
var body: some View {
if redactionReasons.contains(.privacy) {
Text("•••••")
} else {
Text(accountBalance, format: .currency(code: "USD"))
}
}
}
Text Rendering Performance
Efficient Text Updates
// GOOD: Separate text views for changing content
VStack {
Text("Static label:")
Text("\(dynamicValue)") // Only this updates
}
// AVOID: Combining static and dynamic in one Text
Text("Static label: \(dynamicValue)") // Whole text re-renders
Large Text Handling
// For very long text, use ScrollView
ScrollView {
Text(veryLongContent)
.textSelection(.enabled)
}
// Or LazyVStack for segmented content
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(paragraphs, id: \.self) { paragraph in
Text(paragraph)
.padding(.bottom)
}
}
}
Accessibility
VoiceOver Customization
Text("5 stars")
.accessibilityLabel("5 out of 5 stars")
Text("$99")
.accessibilityLabel("99 dollars")
// Heading level
Text("Section Title")
.accessibilityAddTraits(.isHeader)
Dynamic Type Support
// Respect user's text size preference
Text("Accessible text")
.font(.body) // Scales with Dynamic Type
// Fixed size (use sparingly)
Text("Fixed size")
.font(.system(size: 14))
.dynamicTypeSize(.large) // Cap at large
// Size range
Text("Limited scaling")
.dynamicTypeSize(.small...(.accessibilityLarge))
Best Practices
1. Use Semantic Fonts
// GOOD: Semantic fonts scale with Dynamic Type
.font(.headline)
.font(.body)
.font(.caption)
// AVOID: Fixed sizes unless necessary
.font(.system(size: 16))
2. Support Markdown for User Content
// Parse user input as Markdown safely
func renderUserContent(_ input: String) -> Text {
if let attributed = try? AttributedString(
markdown: input,
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
) {
return Text(attributed)
}
return Text(input)
}
3. Enable Text Selection for Copyable Content
Text(address)
.textSelection(.enabled)
4. Handle Localization Properly
// Use LocalizedStringKey for user-facing text
Text("button_title")
// Use verbatim for data
Text(verbatim: userGeneratedContent)
5. Consider Privacy
Text(sensitiveInfo)
.privacySensitive()
Official Resources
More from bluewaves-creations/bluewaves-skills
photographer-testino
Generate images in Mario Testino's glamorous vibrant style. Use when users ask for Testino style, high fashion glamour, bold saturated colors, warm luxurious photography, dynamic sensual energy.
35photographer-lindbergh
Generate images in Peter Lindbergh's iconic black and white style. Use when users ask for Lindbergh style, raw authentic beauty, emotional B&W portraits, supermodel aesthetic, or unretouched natural photography.
30photographer-lachapelle
Generate images in David LaChapelle's surreal pop art style. Use when users ask for LaChapelle style, pop surrealism, hyper-saturated colors, theatrical staging, baroque maximalism, kitsch aesthetic.
24epub-creator
Create production-quality EPUB 3 ebooks from markdown and images with automated QA, formatting fixes, and validation. Use when creating ebooks, converting markdown to EPUB, or compiling chapters into a publishable book. Handles markdown quirks, generates TOC, adds covers, and validates output automatically.
22photographer-vonunwerth
Generate images in Ellen von Unwerth's playful vintage style. Use when users ask for von Unwerth style, playful sensuality, vintage film noir, whimsical feminine photography, retro glamour, narrative storytelling.
19photographer-ritts
Generate images in Herb Ritts' sculptural black and white style. Use when users ask for Ritts style, classical Greek aesthetic, sculptural body photography, California golden hour, minimalist athletic portraits.
18