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.
/plugin marketplace add bluewaves-creations/bluewaves-skills/plugin install swift-apple-dev@bluewaves-skillsThis skill is limited to using the following tools:
Comprehensive guide to SwiftUI text rendering, AttributedString, native Markdown support, and rich text editing for iOS 26 development.
// 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
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("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("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("Aligned text")
.multilineTextAlignment(.leading)
.multilineTextAlignment(.center)
.multilineTextAlignment(.trailing)
// Frame alignment for single line
Text("Single")
.frame(maxWidth: .infinity, alignment: .leading)
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**")
// 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")
// 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)
}
// 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")
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
}
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
var greeting = AttributedString("Hello ")
greeting.font = .title
var name = AttributedString("World")
name.font = .title.bold()
name.foregroundColor = .blue
let combined = greeting + name
Text(combined)
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)")
}
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)
// Full Markdown (default)
.interpretedSyntax: .full
// Inline only (no block elements)
.interpretedSyntax: .inlineOnly
// Inline, preserving whitespace
.interpretedSyntax: .inlineOnlyPreservingWhitespace
do {
let attributed = try AttributedString(markdown: source)
// Use attributed string
} catch {
// Fallback to plain text
let plain = AttributedString(source)
}
// 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
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)
}
}
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
}
}
}
}
iOS 26 TextEditor supports standard keyboard shortcuts:
TextEditor(text: $content, selection: $selection)
.environment(\.fontResolutionContext, FontResolutionContext(
defaultFont: .body,
defaultForegroundColor: .primary
))
// 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))
let name = PersonNameComponents(givenName: "John", familyName: "Doe")
Text(name, format: .name(style: .long))
Text(fileSize, format: .byteCount(style: .file))
// 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"))
Modern localization uses String Catalogs:
// In String Catalog (Localizable.xcstrings)
// Key: "items_count"
// English: "%lld items"
// French: "%lld éléments"
Text("items_count \(count)")
// In String Catalog, define variants:
// "items_count" with plural variants:
// - zero: "No items"
// - one: "1 item"
// - other: "%lld items"
Text("items_count \(count)")
// Localized with attributes
let attributed = AttributedString(localized: "formatted_message")
Text(attributed)
Text("Selectable text that users can copy")
.textSelection(.enabled)
// Disable selection
Text("Not selectable")
.textSelection(.disabled)
List(items) { item in
Text(item.content)
.textSelection(.enabled)
}
@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("Input", text: $text)
.textFieldStyle(.automatic)
.textFieldStyle(.plain)
.textFieldStyle(.roundedBorder)
SecureField("Password", text: $password)
// 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())
@FocusState private var isFocused: Bool
TextField("Input", text: $text)
.focused($isFocused)
Button("Focus") {
isFocused = true
}
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)
TextField("Search", text: $query)
.onSubmit {
performSearch()
}
.submitLabel(.search)
// Submit labels: .done, .go, .join, .next, .return, .search, .send
Label("Settings", systemImage: "gear")
Label("Document", image: "doc-icon")
// Custom label
Label {
Text("Custom")
.font(.headline)
} icon: {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
}
Label("Title", systemImage: "star")
.labelStyle(.automatic)
.labelStyle(.titleOnly)
.labelStyle(.iconOnly)
.labelStyle(.titleAndIcon)
Link("Apple", destination: URL(string: "https://apple.com")!)
Link(destination: URL(string: "https://apple.com")!) {
Label("Visit Apple", systemImage: "safari")
}
// 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)
Text(sensitiveData)
.privacySensitive()
// Manual redaction
Text("Hidden Content")
.redacted(reason: .privacy)
.redacted(reason: .placeholder)
// Unredacted
Text("Always Visible")
.unredacted()
struct ContentView: View {
@Environment(\.redactionReasons) var redactionReasons
var body: some View {
if redactionReasons.contains(.privacy) {
Text("•••••")
} else {
Text(accountBalance, format: .currency(code: "USD"))
}
}
}
// 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
// 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)
}
}
}
Text("5 stars")
.accessibilityLabel("5 out of 5 stars")
Text("$99")
.accessibilityLabel("99 dollars")
// Heading level
Text("Section Title")
.accessibilityAddTraits(.isHeader)
// 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))
// GOOD: Semantic fonts scale with Dynamic Type
.font(.headline)
.font(.body)
.font(.caption)
// AVOID: Fixed sizes unless necessary
.font(.system(size: 16))
// 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)
}
Text(address)
.textSelection(.enabled)
// Use LocalizedStringKey for user-facing text
Text("button_title")
// Use verbatim for data
Text(verbatim: userGeneratedContent)
Text(sensitiveInfo)
.privacySensitive()
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.