swiftship/internal/skills/data/ui/text-formatting/SKILL.md
Text formatting: AttributedString, markdown, interpolation, localization. Use when implementing UI patterns related to text formatting.
npx skillsauth add abdullah4ai/apple-developer-toolkit text-formattingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Never use C-style String(format:) with Text. Always use format parameters.
let value = 42.12345
// Modern (Correct)
Text(value, format: .number.precision(.fractionLength(2)))
// Output: "42.12"
Text(abs(value), format: .number.precision(.fractionLength(2)))
// Output: "42.12" (absolute value)
// Legacy (Avoid)
Text(String(format: "%.2f", abs(value)))
let count = 1234567
// With grouping separator
Text(count, format: .number)
// Output: "1,234,567" (locale-dependent)
// Without grouping
Text(count, format: .number.grouping(.never))
// Output: "1234567"
let price = 19.99
// Fixed decimal places
Text(price, format: .number.precision(.fractionLength(2)))
// Output: "19.99"
// Significant digits
Text(price, format: .number.precision(.significantDigits(3)))
// Output: "20.0"
// Integer-only
Text(price, format: .number.precision(.integerLength(1...)))
// Output: "19"
let price = 19.99
// Correct - with currency code
Text(price, format: .currency(code: "USD"))
// Output: "$19.99"
// With locale
Text(price, format: .currency(code: "EUR").locale(Locale(identifier: "de_DE")))
// Output: "19,99 €"
// Avoid - manual formatting
Text(String(format: "$%.2f", price))
let percentage = 0.856
// Correct - with precision
Text(percentage, format: .percent.precision(.fractionLength(1)))
// Output: "85.6%"
// Without decimal places
Text(percentage, format: .percent.precision(.fractionLength(0)))
// Output: "86%"
// Avoid - manual calculation
Text(String(format: "%.1f%%", percentage * 100))
let date = Date()
// Date only
Text(date, format: .dateTime.day().month().year())
// Output: "Jan 23, 2026"
// Full date
Text(date, format: .dateTime.day().month(.wide).year())
// Output: "January 23, 2026"
// Short date
Text(date, style: .date)
// Output: "1/23/26"
let date = Date()
// Time only
Text(date, format: .dateTime.hour().minute())
// Output: "2:30 PM"
// With seconds
Text(date, format: .dateTime.hour().minute().second())
// Output: "2:30:45 PM"
// 24-hour format
Text(date, format: .dateTime.hour(.defaultDigits(amPM: .omitted)).minute())
// Output: "14:30"
let futureDate = Date().addingTimeInterval(3600)
// Relative formatting
Text(futureDate, style: .relative)
// Output: "in 1 hour"
Text(futureDate, style: .timer)
// Output: "59:59" (counts down)
Use localizedStandardContains() for user-input filtering, not contains().
let searchText = "café"
let items = ["Café Latte", "Coffee", "Tea"]
// Correct - handles diacritics and case
let filtered = items.filter { $0.localizedStandardContains(searchText) }
// Matches "Café Latte"
// Wrong - exact match only
let filtered = items.filter { $0.contains(searchText) }
// Might not match "Café Latte" depending on normalization
Why: localizedStandardContains() handles case-insensitive, diacritic-insensitive matching appropriate for user-facing search.
let text = "Hello World"
let search = "hello"
// Correct - case-insensitive
if text.localizedCaseInsensitiveContains(search) {
// Match found
}
// Also correct - for exact comparison
if text.lowercased() == search.lowercased() {
// Equal
}
let names = ["Zoë", "Zara", "Åsa"]
// Correct - locale-aware sorting
let sorted = names.sorted { $0.localizedStandardCompare($1) == .orderedAscending }
// Output: ["Åsa", "Zara", "Zoë"]
// Wrong - byte-wise sorting
let sorted = names.sorted()
// Output may not be correct for all locales
// Using Text concatenation
Text("Hello ")
.foregroundStyle(.primary)
+ Text("World")
.foregroundStyle(.blue)
.bold()
// Using AttributedString
var attributedString = AttributedString("Hello World")
attributedString.foregroundColor = .primary
if let range = attributedString.range(of: "World") {
attributedString[range].foregroundColor = .blue
attributedString[range].font = .body.bold()
}
Text(attributedString)
// Simple markdown
Text("This is **bold** and this is *italic*")
// With links
Text("Visit [Apple](https://apple.com) for more info")
// Multiline markdown
Text("""
# Title
This is a paragraph with **bold** text.
- Item 1
- Item 2
""")
// Wrong (Legacy) - GeometryReader trick
struct MeasuredText: View {
let text: String
@State private var textHeight: CGFloat = 0
var body: some View {
Text(text)
.background(
GeometryReader { geometry in
Color.clear
.onAppear {
textWidth = geometry.size.height
}
}
)
}
}
// Modern (correct)
struct MeasuredText: View {
let text: String
@State private var textHeight: CGFloat = 0
var body: some View {
Text(text)
.onGeometryChange(for: CGFloat.self) { geometry in
geometry.size.height
} action: { newValue in
textHeight = newValue
}
}
}
.format parameters with Text instead of String(format:).currency(code:) for currency formatting.percent for percentage formatting.dateTime for date/time formattinglocalizedStandardContains() for user-input searchlocalizedStandardCompare() for locale-aware sortingWhy: Modern format parameters are type-safe, localization-aware, and integrate better with SwiftUI's text rendering.
tools
Apple platform skill for docs, WWDC lookup, App Store Connect work, and SwiftUI app generation. Use repo-local `node cli.js` for Apple docs and WWDC search, `appledev store` for App Store Connect workflows, and `appledev build` for app scaffolding or fix loops on macOS. USE WHEN: Apple APIs, WWDC sessions, TestFlight/App Store tasks, or building/fixing Apple-platform apps. DON'T USE WHEN: non-Apple platforms, generic backend work, or general web research. EDGE CASES: docs-only queries use `node cli.js` in this repo, not `appledev`; release workflows use `appledev store`; app scaffolding uses `appledev build`; rules-only requests can read `references/ios-rules/` or `references/swiftui-guides/` progressively without invoking binaries.
tools
All-in-one Apple developer skill with three integrated tools shipped as a single unified binary. (1) Documentation search across Apple frameworks, symbols, and 1,267 WWDC sessions from 2014-2025. No credentials needed. (2) App Store Connect CLI with 120+ commands covering builds (find/wait/upload), TestFlight, pre-submission validate, submissions, signing, subscriptions (family-sharable), IAP, analytics, Xcode Cloud, metadata workflows, release pipeline dashboard, insights, win-back offers, promoted purchases, product pages, nominations, accessibility declarations, pre-orders, pricing filters, localizations update, diff, webhooks with local receiver, workflow automation, and more. Requires App Store Connect API key. (3) Multi-platform app builder (iOS/watchOS/tvOS/iPad/macOS/visionOS) that generates complete Swift/SwiftUI apps from natural language with auto-fix, simulator launch, interactive chat mode, and open-in-Xcode. Requires an LLM API key and Xcode. Includes 38 iOS development rules and 12 SwiftUI best practice guides for Liquid Glass, navigation, state management, and modern APIs. All three tools ship as one binary (appledev). USE WHEN: Apple API docs, App Store Connect management, WWDC lookup, or building iOS/watchOS/tvOS/macOS/visionOS apps from scratch. DON'T USE WHEN: non-Apple platforms or general coding.
testing
watchOS complications: WidgetKit complication families, accessory sizes, timeline providers for watch face. Use when implementing watchOS-specific patterns related to widgets.
development
watchOS haptic feedback: WKInterfaceDevice preset haptic types for wrist-based feedback. Use when implementing watchOS-specific patterns related to haptics.