From apple-kit-skills
Formats and parses values for display using Swift's FormatStyle protocol and Foundation's concrete styles for numbers, currencies, dates, durations, measurements, and more. Useful when building iOS/macOS apps that need locale-aware, type-safe formatting.
How this skill is triggered — by the user, by Claude, or both
Slash command
/apple-kit-skills:swift-formatstyleThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Format values for human-readable display using the `FormatStyle` protocol
Format values for human-readable display using the FormatStyle protocol
and Foundation's concrete format styles. Replaces legacy Formatter subclasses
with a type-safe, composable, cacheable API.
Locale-aware display is an i18n concern even when the app is not adding new languages. When reviewing user-facing FormatStyle output or SwiftUI Text(_:format:), always include an actionable preview/test step for the exact rendered UI in representative locales such as en_US, de_DE, ar_SA, and ja_JP; check separators, numbering systems, calendars, currency and unit conventions, text direction, and layout-sensitive output. Keep this skill focused on FormatStyle, ParseableFormatStyle, parsing, and reusable formatter API design; route broader localization work such as String Catalogs, bundles, plurals, localized copy, and RTL layout review to ios-localization. Do not use "not adding languages" as the reason to skip ios-localization; locale-sensitive formatting can be a localization review issue without translation work.
Docs: FormatStyle
| Type | Style Access | Example |
|---|---|---|
Int, Double | .number | 42.formatted(.number.precision(.fractionLength(2))) → "42.00" |
Decimal | .number, .percent, .currency(code:) | Decimal(string: "0.1")!.formatted(.percent) -> "10%" |
| Currency | .currency(code:) | 29.99.formatted(.currency(code: "USD")) -> "$29.99" |
| Percent | .percent | 0.85.formatted(.percent) → "85%" |
Date | .dateTime | Date.now.formatted(.dateTime.month().day().year()) |
| Date range | .interval | (date1..<date2).formatted(.interval) |
| Relative date | .relative(presentation:unitsStyle:) | date.formatted(.relative(presentation: .named)) → "yesterday" |
Duration | .time(pattern:) | Duration.seconds(3661).formatted(.time(pattern: .hourMinuteSecond)) → "1:01:01" |
Duration | .units(allowed:width:) | Duration.seconds(90).formatted(.units(allowed: [.minutes, .seconds])) → "1 min, 30 sec" |
Measurement | .measurement(width:) | Measurement(value: 72, unit: UnitTemperature.fahrenheit).formatted(.measurement(width: .abbreviated)) |
PersonNameComponents | .name(style:) | name.formatted(.name(style: .short)) → "Tom" |
[String] | .list(type:width:) | ["A","B","C"].formatted(.list(type: .and)) → "A, B, and C" |
| Byte count | .byteCount(style:) | Int64(1_048_576).formatted(.byteCount(style: .memory)) → "1 MB" |
URL | .url | url.formatted(.url.scheme(.never).host().path()) |
// Default locale-aware formatting
let n = 1234567.formatted() // "1,234,567" (en_US)
// Precision
1234.5.formatted(.number.precision(.fractionLength(0...2))) // "1,234.5"
1234.5.formatted(.number.precision(.significantDigits(3))) // "1,230"
// Rounding
1234.formatted(.number.rounded(rule: .down, increment: 100)) // "1,200"
// Grouping
1234567.formatted(.number.grouping(.never)) // "1234567"
// Notation
1_200_000.formatted(.number.notation(.compactName)) // "1.2M"
42.formatted(.number.notation(.scientific)) // "4.2E1"
// Sign display
(-42).formatted(.number.sign(strategy: .always())) // "+42" / "-42"
// Locale override
42.formatted(.number.locale(Locale(identifier: "de_DE"))) // "42"
Docs: IntegerFormatStyle, FloatingPointFormatStyle
Use Decimal.FormatStyle for exact decimal values, especially money-like values
that should not pass through binary floating-point.
let amount = Decimal(string: "12345.67")!
amount.formatted(.number) // "12,345.67" (en_US)
amount.formatted(.number.grouping(.never)) // "12345.67"
Decimal(string: "0.1")!.formatted(.percent) // "10%"
amount.formatted(.currency(code: "USD")) // "$12,345.67"
// Parsing with the same style
let price = try? Decimal("$3,500.63", format: .currency(code: "USD"))
Docs: Decimal.FormatStyle
29.99.formatted(.currency(code: "USD")) // "$29.99"
29.99.formatted(.currency(code: "EUR")) // "€29.99"
29.99.formatted(.currency(code: "JPY")) // "¥30"
// Customize precision
let style = FloatingPointFormatStyle<Double>.Currency(code: "USD")
.precision(.fractionLength(0))
1234.56.formatted(style) // "$1,235"
0.85.formatted(.percent) // "85%"
0.8567.formatted(.percent.precision(.fractionLength(1))) // "85.7%"
42.formatted(.percent) // "42%" (integer)
let now = Date.now
// Components
now.formatted(.dateTime.year().month().day()) // "Apr 22, 2026"
now.formatted(.dateTime.hour().minute()) // "4:30 PM"
now.formatted(.dateTime.weekday(.wide).month(.wide).day()) // "Wednesday, April 22"
// Predefined styles
now.formatted(date: .long, time: .shortened) // "April 22, 2026 at 4:30 PM"
now.formatted(date: .abbreviated, time: .omitted) // "Apr 22, 2026"
// ISO 8601
now.formatted(.iso8601) // "2026-04-22T16:30:00Z"
// Relative
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: .now)!
yesterday.formatted(.relative(presentation: .named)) // "yesterday"
yesterday.formatted(.relative(presentation: .numeric)) // "1 day ago"
// Treat relative strings as standalone text; embedding them inside another
// sentence can be grammatically wrong in some locales. Recommend previewing or
// testing the exact screen in representative locales before approving copy.
Text(yesterday, format: .relative(presentation: .named))
// Interval
(date1..<date2).formatted(.interval.month().day().hour().minute())
// Components (countdown-style)
(date1..<date2).formatted(.components(style: .wide, fields: [.day, .hour]))
// "2 days, 5 hours"
Docs: Date.FormatStyle, Date.RelativeFormatStyle, Date.IntervalFormatStyle
Date.AnchoredRelativeFormatStyle formats relative to a fixed anchor date
rather than the current moment. It requires iOS 18+.
Docs: Date.AnchoredRelativeFormatStyle
Duration (iOS 16+) has two format styles:
Docs: Duration.TimeFormatStyle, Duration.UnitsFormatStyle
let d = Duration.seconds(3661)
d.formatted(.time(pattern: .hourMinuteSecond)) // "1:01:01"
d.formatted(.time(pattern: .hourMinute)) // "1:01"
d.formatted(.time(pattern: .minuteSecond)) // "61:01"
// Fractional seconds
Duration.seconds(3.75).formatted(
.time(pattern: .minuteSecond(padMinuteToLength: 2, fractionalSecondsLength: 2))
) // "00:03.75"
Duration.seconds(3661).formatted(
.units(allowed: [.hours, .minutes, .seconds], width: .abbreviated)
) // "1 hr, 1 min, 1 sec"
Duration.seconds(90).formatted(
.units(allowed: [.minutes, .seconds], width: .wide)
) // "1 minute, 30 seconds"
Duration.seconds(90).formatted(
.units(allowed: [.minutes, .seconds], width: .narrow)
) // "1m 30s"
// Limit unit count
Duration.seconds(3661).formatted(
.units(allowed: [.hours, .minutes, .seconds], width: .abbreviated, maximumUnitCount: 2)
) // "1 hr, 1 min"
let temp = Measurement(value: 72, unit: UnitTemperature.fahrenheit)
temp.formatted(.measurement(width: .wide)) // "72 degrees Fahrenheit"
temp.formatted(.measurement(width: .abbreviated)) // "72°F"
temp.formatted(.measurement(width: .narrow)) // "72°"
let dist = Measurement(value: 5, unit: UnitLength.kilometers)
dist.formatted(.measurement(width: .abbreviated, usage: .road)) // "3.1 mi" (en_US)
Docs: Measurement.FormatStyle
var name = PersonNameComponents()
name.givenName = "Thomas"
name.familyName = "Clark"
name.middleName = "Louis"
name.namePrefix = "Dr."
name.nickname = "Tom"
name.nameSuffix = "Esq."
name.formatted(.name(style: .long)) // "Dr. Thomas Louis Clark Esq."
name.formatted(.name(style: .medium)) // "Thomas Clark"
name.formatted(.name(style: .short)) // "Tom"
name.formatted(.name(style: .abbreviated)) // "TC"
Style resolution follows priority: script → user preferences → locale → developer setting.
Docs: PersonNameComponents.FormatStyle
["Alice", "Bob", "Charlie"].formatted(.list(type: .and))
// "Alice, Bob, and Charlie"
["Alice", "Bob", "Charlie"].formatted(.list(type: .or))
// "Alice, Bob, or Charlie"
// With member formatting
[1, 2, 3].formatted(.list(memberStyle: .number, type: .and))
// "1, 2, and 3"
// Narrow width
["A", "B", "C"].formatted(.list(type: .and, width: .narrow))
// "A, B, C"
Docs: ListFormatStyle
Int64(1_048_576).formatted(.byteCount(style: .memory)) // "1 MB"
Int64(1_048_576).formatted(.byteCount(style: .file)) // "1 MB"
Int64(1_048_576).formatted(.byteCount(style: .binary)) // "1 MiB"
Docs: ByteCountFormatStyle
URL.FormatStyle requires iOS 16+. The default style includes scheme, host,
and path. Treat port, query, and fragment as opt-in display components; add
.port(.always), .query(.always), or .fragment(.always) only when those
components should be visible.
let url = URL(string: "https://www.example.com:8080/path?q=1#section")!
url.formatted()
// "https://www.example.com/path"
url.formatted(.url.scheme(.never).host().path())
// "www.example.com/path"
url.formatted(.url.scheme(.never).host().path().query(.always))
// "www.example.com/path?q=1"
url.formatted(.url.scheme(.never).host().path().fragment(.always))
// "www.example.com/path#section"
When auditing URL component choices, recommend previewing or testing the exact rendered URL text in representative locales, especially when choosing whether to show or hide scheme, path, query, port, or fragment.
Docs: URL.FormatStyle
Text accepts a format: parameter, keeping formatting out of the view model.
// Inline format style
Text(price, format: .currency(code: "USD"))
Text(date, format: .dateTime.month().day().year())
Text(duration, format: .units(allowed: [.minutes, .seconds]))
// Timer-style (live updating)
Text(.now, style: .timer)
Text(.now, style: .relative)
Text(timerInterval: start...end)
Prefer Text(_:format:) over string interpolation — it allows SwiftUI to
re-render only the formatted value and supports accessibility scaling.
For every SwiftUI formatted Text review, include a representative-locale
preview, UI test, or snapshot test recommendation for the exact screen.
Conform to FormatStyle for domain-specific formatting. Conform to
ParseableFormatStyle if you also need parsing. FormatStyle refines
Decodable, Encodable, and Hashable, and Foundation caches identical
customized style instances, so reusable value-style formatters are cheap.
struct AbbreviatedCountStyle: FormatStyle {
func format(_ value: Int) -> String {
switch value {
case ..<1_000:
return "\(value)"
case 1_000..<1_000_000:
return String(format: "%.1fK", Double(value) / 1_000)
default:
return String(format: "%.1fM", Double(value) / 1_000_000)
}
}
}
extension FormatStyle where Self == AbbreviatedCountStyle {
static var abbreviatedCount: AbbreviatedCountStyle { .init() }
}
// Usage
let followers = 12_500
Text(followers, format: .abbreviatedCount) // "12.5K"
For parseable custom styles, pair formatting with a parse strategy and use the
same conventions in both directions. Prefer this only when users edit or import
the formatted value; display-only styles should stay as FormatStyle.
Docs: ParseableFormatStyle
| Mistake | Fix |
|---|---|
Using legacy NumberFormatter / DateFormatter in new code | Use FormatStyle (iOS 15+). Foundation caches format style instances automatically. |
String interpolation for formatted numbers in Text | Use Text(value, format:) for locale correctness and accessibility |
| Hardcoding locale in format styles | Omit .locale() to inherit the user's current locale by default |
Assuming URL.formatted() preserves query strings, ports, or fragments | Default URL formatting includes scheme, host, and path only; opt in with .query(.always), .port(.always), or .fragment(.always) |
| Embedding relative date output inside larger sentences | Use Date.RelativeFormatStyle output as standalone text; localized grammar may not fit interpolation |
| Forgetting availability checks | URL.FormatStyle and Duration format styles require iOS 16+; Date.AnchoredRelativeFormatStyle requires iOS 18+ |
Using .time(pattern:) for labeled duration display | Use .units(allowed:width:) for "1 hr, 30 min" style output |
Creating Formatter instances in body or tight loops | FormatStyle instances are value types cached by Foundation; safe to create inline |
Formatting Duration with DateComponentsFormatter | Use Duration.TimeFormatStyle or Duration.UnitsFormatStyle directly |
Ignoring usage: parameter for measurements | Specify .road, .asProvided, etc. for locale-aware unit conversion |
| Using binary floating-point for exact decimal display/parsing | Use Decimal.FormatStyle and matching parse strategies for exact decimal values |
FormatStyle used instead of legacy Formatter subclasses for iOS 15+ targetsURL.FormatStyle and Duration styles gated to iOS 16+; anchored relative dates gated to iOS 18+Text(_:format:) used instead of pre-formatting strings for SwiftUI textText includes an explicit representative-locale preview/test recommendationDecimal.FormatStyle when exact decimal formatting or parsing mattersDuration.TimeFormatStyle or Duration.UnitsFormatStyleusage: for user-facing displayCodable + Hashable for cachingnpx claudepluginhub dpearson2699/swift-ios-skills --plugin all-ios-skillsUse this skill for any task involving Thai date formatting, Buddhist Era (พ.ศ.) ↔ Gregorian (ค.ศ.) year conversion, or Arabic ↔ Thai numeral conversion. Trigger whenever the user asks to: convert พ.ศ. to ค.ศ. or vice versa, format a date in Thai government / business / casual style, parse a Thai date string with full or abbreviated month names, or convert digits between 0123456789 and ๐๑๒๓๔๕๖๗๘๙. Also trigger for requests like "แปลง พ.ศ. เป็น ค.ศ.", "จัดรูปแบบวันที่ไทย", "วันที่แบบราชการ", "เปลี่ยนเป็นเลขไทย", or any variation involving Thai calendar dates.
Formats dates into localized strings respecting user language and region settings. Provides fine-grained control over date parts and formatting options like slashes or capitalization.
Localizes iOS/macOS apps with String Catalogs, generated symbols, pluralization, FormatStyle, and RTL layout. Useful for multi-language support and avoiding localization mistakes.