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.
testing
Use for 3D games: racing, 3D sports, board games, marble maze, tower defense, bowling. SceneKit + SceneView architecture, 3D scene hierarchy, physics, game loop, primitives, materials, cameras, particles, audio.
documentation
Game UI patterns: SwiftUI HUD overlays on SpriteKit, menus (main/pause/game-over), virtual joystick/d-pad, score displays, health bars, tutorial onboarding.
tools
Download free game sprites/textures/3D models and generate procedural assets. Covers nw_download_asset tool, texture factories, sprite atlas organization, 3D model loading, and programmatic asset creation.
testing
Use for 2D games: arcade, puzzle, sports, ping pong, platformer, shooter, 2D racing. SpriteKit + SpriteView architecture, scene hierarchy, physics, game loop, audio, particles, game feel.