Agent skill
text-rich-content
SwiftUI Text rendering, AttributedString, native Markdown support, and rich text editing. Use when user asks about Text, Markdown, AttributedString, rich text, TextEditor, text formatting, or localization.
Stars
163
Forks
31
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/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
swift
// 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
swift
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
swift
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
swift
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
swift
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:
swift
// 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
swift
// 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
swift
// 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
swift
// 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
swift
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
swift
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
swift
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
swift
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
swift
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
swift
// Full Markdown (default)
.interpretedSyntax: .full
// Inline only (no block elements)
.interpretedSyntax: .inlineOnly
// Inline, preserving whitespace
.interpretedSyntax: .inlineOnlyPreservingWhitespace
Handling Parse Errors
swift
do {
let attributed = try AttributedString(markdown: source)
// Use attributed string
} catch {
// Fallback to plain text
let plain = AttributedString(source)
}
Custom Attribute Scopes
swift
// 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:
swift
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
swift
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
swift
TextEditor(text: $content, selection: $selection)
.environment(\.fontResolutionContext, FontResolutionContext(
defaultFont: .body,
defaultForegroundColor: .primary
))
Text Interpolation
Format Styles
swift
// 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
swift
let name = PersonNameComponents(givenName: "John", familyName: "Doe")
Text(name, format: .name(style: .long))
ByteCount
swift
Text(fileSize, format: .byteCount(style: .file))
Localization
LocalizedStringKey
swift
// 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:
swift
// In String Catalog (Localizable.xcstrings)
// Key: "items_count"
// English: "%lld items"
// French: "%lld éléments"
Text("items_count \(count)")
Pluralization
swift
// 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
swift
// Localized with attributes
let attributed = AttributedString(localized: "formatted_message")
Text(attributed)
Text Selection
Enabling Selection
swift
Text("Selectable text that users can copy")
.textSelection(.enabled)
// Disable selection
Text("Not selectable")
.textSelection(.disabled)
Selection on Lists
swift
List(items) { item in
Text(item.content)
.textSelection(.enabled)
}
TextField and SecureField
Basic TextField
swift
@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
swift
TextField("Input", text: $text)
.textFieldStyle(.automatic)
.textFieldStyle(.plain)
.textFieldStyle(.roundedBorder)
SecureField
swift
SecureField("Password", text: $password)
Formatting TextField
swift
// 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
swift
@FocusState private var isFocused: Bool
TextField("Input", text: $text)
.focused($isFocused)
Button("Focus") {
isFocused = true
}
Keyboard Types
swift
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
swift
TextField("Search", text: $query)
.onSubmit {
performSearch()
}
.submitLabel(.search)
// Submit labels: .done, .go, .join, .next, .return, .search, .send
Label
Basic Label
swift
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
swift
Label("Title", systemImage: "star")
.labelStyle(.automatic)
.labelStyle(.titleOnly)
.labelStyle(.iconOnly)
.labelStyle(.titleAndIcon)
Link
Basic Links
swift
Link("Apple", destination: URL(string: "https://apple.com")!)
Link(destination: URL(string: "https://apple.com")!) {
Label("Visit Apple", systemImage: "safari")
}
Links in Text
swift
// 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
swift
Text(sensitiveData)
.privacySensitive()
// Manual redaction
Text("Hidden Content")
.redacted(reason: .privacy)
.redacted(reason: .placeholder)
// Unredacted
Text("Always Visible")
.unredacted()
Conditional Redaction
swift
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
swift
// 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
swift
// 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
swift
Text("5 stars")
.accessibilityLabel("5 out of 5 stars")
Text("$99")
.accessibilityLabel("99 dollars")
// Heading level
Text("Section Title")
.accessibilityAddTraits(.isHeader)
Dynamic Type Support
swift
// 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
swift
// 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
swift
// 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
swift
Text(address)
.textSelection(.enabled)
4. Handle Localization Properly
swift
// Use LocalizedStringKey for user-facing text
Text("button_title")
// Use verbatim for data
Text(verbatim: userGeneratedContent)
5. Consider Privacy
swift
Text(sensitiveInfo)
.privacySensitive()
Official Resources
Didn't find tool you were looking for?