text-rich-content
SKILL.md
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
Weekly Installs
3
Repository
bluewaves-creat…s-skillsGitHub Stars
1
First Seen
Jan 26, 2026
Security Audits
Installed on
opencode3
codex1
claude-code1
gemini-cli1