Swift FormatStyle
Use Foundation's type-safe FormatStyle APIs for user-facing display and ParseableFormatStyle when the same convention must accept input. Route String Catalogs, plurals, localized copy, bundles, and RTL layout to ios-localization; formatting still requires locale review even when no text is translated.
Contents
- [Workflow](#workflow)
- [Quick Reference](#quick-reference)
- [Selection and Availability](#selection-and-availability)
- [Parsing](#parsing)
- [SwiftUI Integration](#swiftui-integration)
- [Custom FormatStyle](#custom-formatstyle)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)
Workflow
- Identify the value's semantic type and whether output is display-only or must round-trip through parsing.
- Select the narrowest built-in style and keep the user's current locale unless a wire format explicitly requires another locale.
- Render the exact UI with representative locales such as
enUS, deDE, arSA, and jaJP.
- Check separators, numbering systems, calendars, currency and unit conventions, text direction, and layout.
- If output or parsing fails, fix the style or surrounding copy, restore the input fixture, and rerun the same locale matrix.
Load [FormatStyle Recipes](references/formatstyle-recipes.md) when implementation needs concrete modifiers, date intervals, relative dates, duration patterns, measurements, names, lists, byte counts, or URL component controls.
Quick Reference
| Value |
Default style |
Important constraint |
Int, Double |
.number |
Configure precision, rounding, grouping, notation, or sign only as required. |
Decimal |
.number, .percent, .currency(code:) |
Prefer for exact decimal display and parsing. |
| Currency |
.currency(code:) |
Pass an ISO 4217 code; never hardcode a symbol. |
| Percent |
.percent |
Confirm whether the input is a fraction (0.85) or whole percent (85). |
Date |
.dateTime, .relative, .interval |
Relative output should usually stand alone, not be embedded in a sentence. |
Duration |
.time(pattern:), .units(allowed:width:) |
Requires iOS 16+; choose compact clock output versus labeled units. |
Measurement |
.measurement(width:usage:) |
usage: controls locale-aware conversion policy. |
PersonNameComponents |
.name(style:) |
Do not manually concatenate name parts. |
| Collections |
.list(type:width:) |
Let locale rules choose separators and conjunctions. |
| Byte count |
.byteCount(style:) |
Choose file, memory, decimal, or binary semantics deliberately. |
URL |
.url |
Requires iOS 16+; query, port, and fragment are opt-in display components. |
Selection and Availability
- Prefer
FormatStyle over legacy NumberFormatter, DateFormatter, DateComponentsFormatter, and manual interpolation for new iOS 15+ code.
Duration format styles and URL.FormatStyle require iOS 16+.
Date.AnchoredRelativeFormatStyle requires iOS 18+ and formats relative to a fixed anchor rather than the current moment.
- Omit
.locale(...) for normal UI so the style inherits the user's locale. Use a fixed locale only for an explicit protocol or test fixture.
- Treat relative date output as standalone text unless the entire sentence is localized around it.
Docs: FormatStyle
Parsing
Use the matching parseable style when users edit or import formatted values:
let style = Decimal.FormatStyle.Currency(code: "USD")
.locale(Locale(identifier: "en_US"))
let value = try Decimal("$3,500.63", format: style)
let display = value.formatted(style)
The fixed locale above is appropriate only because the input contract is explicitly en_US. For normal UI input, use the user's locale and test round trips with representative decimal separators, currency placement, calendars, and numbering systems.
SwiftUI Integration
Prefer Text(_:format:) so SwiftUI owns the formatted value:
Text(price, format: .currency(code: currencyCode))
Text(date, format: .dateTime.month().day().year())
Text(duration, format: .units(allowed: [.minutes, .seconds]))
For every user-facing formatted Text, preview or test the exact screen across the locale matrix. Use Text(.now, style: .timer), Text(.now, style: .relative), or Text(timerInterval:) for live-updating time displays rather than manually scheduling string refreshes.
Custom FormatStyle
Create a custom style only when built-in composition cannot express the domain convention. FormatStyle refines Codable and Hashable; keep styles as reusable values and add ParseableFormatStyle only when input must round-trip.
struct AbbreviatedCountStyle: FormatStyle {
func format(_ value: Int) -> String {
switch value {
case ..<1_000: "\(value)"
case 1_000..<1_000_000: String(format: "%.1fK", Double(value) / 1_000)
default: String(format: "%.1fM", Double(value) / 1_000_000)
}
}
}
extension FormatStyle where Self == AbbreviatedCountStyle {
static var abbreviatedCount: Self { .init() }
}
Custom styles still need locale and accessibility review; a compact English suffix may not be suitable for every locale.
Common Mistakes
| Mistake |
Fix |
| Manual formatting or legacy formatter allocation in view code |
Use the matching FormatStyle and Text(_:format:). |
| Hardcoded currency symbol or locale |
Pass an ISO currency code and inherit the user locale unless the contract says otherwise. |
| Binary floating-point for exact decimal input |
Use Decimal.FormatStyle and its matching parse strategy. |
Assuming URL.formatted() preserves every component |
Opt in to .port(.always), .query(.always), or .fragment(.always) only when those components should display. |
| Relative date output embedded in a larger sentence |
Keep it standalone or localize the complete sentence. |
| Clock-style duration used for prose |
Use .units(allowed:width:) for labeled output. |
| Measurement conversion left implicit |
Select a usage: appropriate to the UI. |
| One-locale spot check |
Run the same exact-screen fixture across representative locales and fix/rerun failures. |
Review Checklist
References