CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

90

Quality

90%

Does it follow best practices?

Impact

Average score across 248 eval scenarios

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

SKILL.mdskills/swift-formatstyle/

name:
swift-formatstyle
description:
Format and parse values for display using the FormatStyle and ParseableFormatStyle protocols and Foundation's concrete styles. Use when formatting numbers (integers, floating-point, decimals), currencies, percentages, dates, date ranges, relative dates, durations (Duration.TimeFormatStyle, Duration.UnitsFormatStyle), measurements, person names (PersonNameComponents.FormatStyle), byte counts (ByteCountFormatStyle), lists (ListFormatStyle), and URLs (URL.FormatStyle). Also covers custom FormatStyle conformances, parse strategies, reusable formatter API design, and replacing legacy Formatter subclasses. FormatStyle is available iOS 15+; Duration and URL styles require iOS 16+.

Swift FormatStyle

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

Contents

Quick Reference

TypeStyle AccessExample
Int, Double.number42.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.percent0.85.formatted(.percent)"85%"
Date.dateTimeDate.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.urlurl.formatted(.url.scheme(.never).host().path())

Numbers

// 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

Decimals

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

Currency

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"

Percentages

0.85.formatted(.percent)                                      // "85%"
0.8567.formatted(.percent.precision(.fractionLength(1)))       // "85.7%"
42.formatted(.percent)                                         // "42%"  (integer)

Dates

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

Anchored Relative Dates (iOS 18+)

Date.AnchoredRelativeFormatStyle formats relative to a fixed anchor date rather than the current moment. It requires iOS 18+.

Docs: Date.AnchoredRelativeFormatStyle

Durations

Duration (iOS 16+) has two format styles:

Docs: Duration.TimeFormatStyle, Duration.UnitsFormatStyle

TimeFormatStyle — compact separator-based

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"

UnitsFormatStyle — labeled units

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"

Measurements

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

Person Names

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

Lists

["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

Byte Counts

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

URLs

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

SwiftUI Integration

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.

Custom FormatStyle

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

Common Mistakes

MistakeFix
Using legacy NumberFormatter / DateFormatter in new codeUse FormatStyle (iOS 15+). Foundation caches format style instances automatically.
String interpolation for formatted numbers in TextUse Text(value, format:) for locale correctness and accessibility
Hardcoding locale in format stylesOmit .locale() to inherit the user's current locale by default
Assuming URL.formatted() preserves query strings, ports, or fragmentsDefault URL formatting includes scheme, host, and path only; opt in with .query(.always), .port(.always), or .fragment(.always)
Embedding relative date output inside larger sentencesUse Date.RelativeFormatStyle output as standalone text; localized grammar may not fit interpolation
Forgetting availability checksURL.FormatStyle and Duration format styles require iOS 16+; Date.AnchoredRelativeFormatStyle requires iOS 18+
Using .time(pattern:) for labeled duration displayUse .units(allowed:width:) for "1 hr, 30 min" style output
Creating Formatter instances in body or tight loopsFormatStyle instances are value types cached by Foundation; safe to create inline
Formatting Duration with DateComponentsFormatterUse Duration.TimeFormatStyle or Duration.UnitsFormatStyle directly
Ignoring usage: parameter for measurementsSpecify .road, .asProvided, etc. for locale-aware unit conversion
Using binary floating-point for exact decimal display/parsingUse Decimal.FormatStyle and matching parse strategies for exact decimal values

Review Checklist

  • FormatStyle used instead of legacy Formatter subclasses for iOS 15+ targets
  • URL.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 text
  • Every user-facing formatted value or SwiftUI formatted Text includes an explicit representative-locale preview/test recommendation
  • No hardcoded locale unless explicitly needed (e.g., server communication)
  • Decimal values use Decimal.FormatStyle when exact decimal formatting or parsing matters
  • URL formatting explicitly includes query, port, or fragment only when those components should display, with representative-locale preview for user-facing URL text
  • Relative date strings are used standalone, not interpolated into larger localized sentences, and previewed in representative locales
  • Duration formatting uses Duration.TimeFormatStyle or Duration.UnitsFormatStyle
  • Currency codes are ISO 4217 strings, not hardcoded symbols
  • Measurement formatting includes usage: for user-facing display
  • Custom FormatStyle types conform to Codable + Hashable for caching

References

skills

swift-formatstyle

README.md

tile.json