Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Comprehensive reference for locale-aware formatting in iOS 15+ using FormatStyle. Never hard-code date, number, or measurement formats -- these break in every locale except the one you tested.
@ScaledMetric for Dynamic TypeFormatStyle (iOS 15+) replaces the older Formatter subclasses. If you encounter legacy code, migrate to FormatStyle:
| Legacy | Modern replacement |
|---|---|
DateFormatter | .formatted(.dateTime...) or Date.FormatStyle |
NumberFormatter | .formatted(.number...) or IntegerFormatStyle / FloatingPointFormatStyle |
DateComponentsFormatter | Duration.formatted(.units(...)) or .time(pattern:) |
MeasurementFormatter | Measurement.formatted(.measurement(...)) |
DateIntervalFormatter | (start..<end).formatted(date:time:) |
PersonNameComponentsFormatter | .formatted(.name(style:)) |
ByteCountFormatter | .formatted(.byteCount(style:)) |
ListFormatter | .formatted(.list(type:)) |
FormatStyle is value-type, Sendable, composable, and works directly in SwiftUI Text views. The legacy formatters are reference types that require manual locale and calendar configuration.
let date = Date.now
// Date only
date.formatted(date: .numeric, time: .omitted) // "1/15/2026" (US) / "15.01.2026" (DE)
date.formatted(date: .abbreviated, time: .omitted) // "Jan 15, 2026" (US) / "15. Jan. 2026" (DE)
date.formatted(date: .long, time: .omitted) // "January 15, 2026" (US) / "15. Januar 2026" (DE)
date.formatted(date: .complete, time: .omitted) // "Thursday, January 15, 2026" (US)
// Time only
date.formatted(date: .omitted, time: .shortened) // "3:30 PM" (US) / "15:30" (DE)
date.formatted(date: .omitted, time: .standard) // "3:30:45 PM" (US) / "15:30:45" (DE)
date.formatted(date: .omitted, time: .complete) // includes time zone
// Combined
date.formatted(date: .long, time: .shortened) // "January 15, 2026 at 3:30 PM"
date.formatted() // platform defaultBuild custom date formats by composing components. The system reorders components for each locale.
// Month and day
date.formatted(.dateTime.month().day()) // "Jan 15" (US) / "15 Jan" (UK)
// Full date with weekday
date.formatted(.dateTime.weekday(.wide).month(.wide).day().year())
// "Thursday, January 15, 2026" (US) / "Donnerstag, 15. Januar 2026" (DE)
// Month name styles
date.formatted(.dateTime.month(.wide)) // "January"
date.formatted(.dateTime.month(.abbreviated)) // "Jan"
date.formatted(.dateTime.month(.narrow)) // "J"
date.formatted(.dateTime.month(.twoDigits)) // "01"
// Day styles
date.formatted(.dateTime.day(.twoDigits)) // "15"
date.formatted(.dateTime.day(.ordinalOfDayInMonth)) // "3" (third Thursday)
// Year
date.formatted(.dateTime.year(.defaultDigits)) // "2026"
date.formatted(.dateTime.year(.twoDigits)) // "26"
// Hour/minute
date.formatted(.dateTime.hour().minute()) // "3:30 PM" (US, 12h) / "15:30" (DE, 24h)
date.formatted(.dateTime.hour(.defaultDigits(amPM: .omitted)).minute()) // "3:30"// Automatic format
Text(event.date, format: .dateTime.month().day().year())
// Date range
Text(event.start...event.end) // "Jan 15 - Jan 20, 2026"
// Relative (auto-updates)
Text(event.date, style: .relative) // "2 hours ago", "in 3 days"
Text(event.date, style: .timer) // counts up/down live
Text(event.date, style: .offset) // "+2 hours" / "-3 days"// Relative (named style)
let relative = date.formatted(.relative(presentation: .named))
// "yesterday", "today", "tomorrow", "last Friday", "in 2 weeks"
// Relative (numeric style)
let relativeNum = date.formatted(.relative(presentation: .numeric))
// "1 day ago", "in 2 days", "3 weeks ago"
// Relative with specific units
let relativeCustom = date.formatted(.relative(presentation: .named, unitsStyle: .wide))let start = Date.now
let end = Calendar.current.date(byAdding: .day, value: 5, to: start)!
// Range formatting
(start..<end).formatted(date: .abbreviated, time: .omitted)
// "Jan 15 - 20, 2026" (smart about shared month/year)
// Duration of interval
(start..<end).formatted(.components(style: .wide))
// "5 days"// For serialization to APIs -- NOT for user-facing display
date.formatted(.iso8601) // "2026-01-15T15:30:45Z"
date.formatted(.iso8601.dateSeparator(.dash).timeSeparator(.colon))let value = 1234567
value.formatted() // "1,234,567" (US) / "1.234.567" (DE) / "1 234 567" (FR)
value.formatted(.number.grouping(.never)) // "1234567"
value.formatted(.number.precision(.significantDigits(3))) // "1,230,000"
let decimal = 3.14159
decimal.formatted(.number.precision(.fractionLength(2))) // "3.14"
decimal.formatted(.number.precision(.fractionLength(0...3))) // "3.142"
// Notation
value.formatted(.number.notation(.compactName)) // "1.2M" (US) / "1,2 Mio." (DE)
value.formatted(.number.notation(.scientific)) // "1.234567E6"let num = 3.456
// Round to 2 fraction digits
num.formatted(.number.precision(.fractionLength(2)).rounded(rule: .up)) // "3.46"
num.formatted(.number.precision(.fractionLength(2)).rounded(rule: .down)) // "3.45"
num.formatted(.number.precision(.fractionLength(2)).rounded(rule: .toNearestOrEven)) // "3.46"let ratio = 0.856
ratio.formatted(.percent) // "86%" (US) / "86 %" (FR)
ratio.formatted(.percent.precision(.fractionLength(1))) // "85.6%"
// Integer percentage
let score = 92
score.formatted(.percent) // "9,200%" -- probably not what you want!
// For integer percentages, divide first:
(Double(score) / 100).formatted(.percent) // "92%"Always specify the currency code explicitly. The locale controls formatting (symbol position, decimal separator), but the currency code determines the currency.
let price = Decimal(29.99)
// Explicit currency code (recommended)
price.formatted(.currency(code: "USD")) // "$29.99" (US) / "29,99 $US" (FR) / "US$29.99" (AU)
price.formatted(.currency(code: "EUR")) // "EUR29.99" (US) / "29,99 EUR" (DE) / "29,99 EUR" (FR)
price.formatted(.currency(code: "JPY")) // "JPY30" (no decimals for yen)
// Narrow symbol (when space is limited)
price.formatted(.currency(code: "USD").presentation(.narrow)) // "$29.99" even in non-US locales
// In SwiftUI
Text(price, format: .currency(code: order.currencyCode))Important: Use Decimal (not Double) for monetary values to avoid floating-point precision errors.
let position = 3
position.formatted(.number.notation(.ordinal)) // "3rd" (EN) / "3." (DE) / "3e" (FR)The system auto-converts units based on locale (metric vs imperial) unless you opt out.
let distance = Measurement(value: 5, unit: UnitLength.kilometers)
distance.formatted(.measurement(width: .wide))
// US: "3.1 miles" (auto-converts to imperial!)
// DE: "5 Kilometer"
// JP: "5 km" (with .abbreviated)
distance.formatted(.measurement(width: .abbreviated)) // "3.1 mi" (US) / "5 km" (DE)
distance.formatted(.measurement(width: .narrow)) // "3.1mi" (US) / "5km" (DE)
// Prevent auto-conversion (keep original unit)
distance.formatted(.measurement(width: .wide, usage: .asProvided))
// US: "5 kilometers" (keeps km even in US locale)let weight = Measurement(value: 75, unit: UnitMass.kilograms)
weight.formatted(.measurement(width: .wide))
// US: "165.3 pounds" / DE: "75 Kilogramm"
weight.formatted(.measurement(width: .abbreviated, usage: .personWeight))
// Uses locale-appropriate unit for body weightlet temp = Measurement(value: 22, unit: UnitTemperature.celsius)
temp.formatted(.measurement(width: .abbreviated))
// US: "72 F" (auto-converts!) / FR: "22 C" / DE: "22 C"
// Weather-specific (ensures locale-correct unit)
temp.formatted(.measurement(width: .abbreviated, usage: .weather))let speed = Measurement(value: 100, unit: UnitSpeed.kilometersPerHour)
speed.formatted(.measurement(width: .abbreviated))
// US: "62.1 mph" / DE: "100 km/h"let volume = Measurement(value: 500, unit: UnitVolume.milliliters)
volume.formatted(.measurement(width: .abbreviated, usage: .drink))
// US: "16.9 fl oz" / DE: "500 ml"let dur = Duration.seconds(3661) // 1 hour, 1 minute, 1 second
dur.formatted(.time(pattern: .hourMinuteSecond)) // "1:01:01"
dur.formatted(.time(pattern: .hourMinute)) // "1:01"
dur.formatted(.time(pattern: .minuteSecond)) // "61:01"dur.formatted(.units(allowed: [.hours, .minutes], width: .wide))
// "1 hour, 1 minute" (EN) / "1 Stunde, 1 Minute" (DE)
dur.formatted(.units(allowed: [.hours, .minutes], width: .abbreviated))
// "1 hr, 1 min" (EN) / "1 Std., 1 Min." (DE)
dur.formatted(.units(allowed: [.hours, .minutes], width: .narrow))
// "1h 1m"
// Maximum unit count
dur.formatted(.units(allowed: [.hours, .minutes, .seconds],
width: .abbreviated,
maximumUnitCount: 2))
// "1 hr, 1 min" (drops seconds)Respects locale conventions for name ordering (given-family vs family-given).
var name = PersonNameComponents()
name.givenName = "John"
name.familyName = "Appleseed"
name.namePrefix = "Dr."
name.nickname = "Johnny"
name.formatted(.name(style: .long)) // "Dr. John Appleseed" (US) / "Appleseed John" (JP)
name.formatted(.name(style: .medium)) // "John Appleseed" (US) / "Appleseed John" (JP)
name.formatted(.name(style: .short)) // "John" (US) / "Appleseed" (JP)
name.formatted(.name(style: .abbreviated)) // "JA" (initials)
// In SwiftUI
Text(name, format: .name(style: .medium))Format file sizes with locale-appropriate units.
let bytes: Int64 = 1_536_000
bytes.formatted(.byteCount(style: .file)) // "1.5 MB"
bytes.formatted(.byteCount(style: .memory)) // "1.46 MB" (uses 1024-based)
bytes.formatted(.byteCount(style: .binary)) // "1.46 MB"
// Specific allowed units
bytes.formatted(.byteCount(style: .file, allowedUnits: [.kb])) // "1,536 kB"Join arrays into grammatically correct lists.
let fruits = ["Apples", "Oranges", "Bananas"]
fruits.formatted(.list(type: .and))
// EN: "Apples, Oranges, and Bananas"
// FR: "Apples, Oranges et Bananas"
// AR: "Apples وOranges وBananas"
fruits.formatted(.list(type: .or))
// EN: "Apples, Oranges, or Bananas"
// With member formatting
let prices = [Decimal(1.99), Decimal(2.49), Decimal(3.99)]
prices.formatted(.list(memberStyle: .currency(code: "USD"), type: .and))
// "$1.99, $2.49, and $3.99"
// Two items
["Red", "Blue"].formatted(.list(type: .and))
// "Red and Blue" (no Oxford comma for two items)Create a reusable FormatStyle for domain-specific formatting.
struct AbbreviatedCountStyle: FormatStyle {
func format(_ value: Int) -> String {
switch value {
case ..<1_000:
return "\(value)"
case 1_000..<1_000_000:
let k = Double(value) / 1_000.0
return k.formatted(.number.precision(.fractionLength(0...1))) + "K"
case 1_000_000..<1_000_000_000:
let m = Double(value) / 1_000_000.0
return m.formatted(.number.precision(.fractionLength(0...1))) + "M"
default:
let b = Double(value) / 1_000_000_000.0
return b.formatted(.number.precision(.fractionLength(0...1))) + "B"
}
}
}
extension FormatStyle where Self == AbbreviatedCountStyle {
static var abbreviatedCount: AbbreviatedCountStyle { .init() }
}
// Usage
let followers = 12_500
followers.formatted(.abbreviatedCount) // "12.5K"
// In SwiftUI
Text(followers, format: .abbreviatedCount)struct AbbreviatedCountStyle: ParseableFormatStyle {
var parseStrategy: AbbreviatedCountParseStrategy { .init() }
func format(_ value: Int) -> String { /* same as above */ }
}
struct AbbreviatedCountParseStrategy: ParseStrategy {
func parse(_ value: String) throws -> Int {
let cleaned = value.uppercased().trimmingCharacters(in: .whitespaces)
if cleaned.hasSuffix("K") {
guard let num = Double(cleaned.dropLast()) else { throw parseError }
return Int(num * 1_000)
}
// ... handle M, B, plain numbers
guard let num = Int(cleaned) else { throw parseError }
return num
}
private var parseError: some Error {
DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Invalid count"))
}
}Occasionally you need a specific locale (server APIs, fixed-format exports). Use .locale() modifier:
// Force US format for API serialization (not user display)
let usPrice = price.formatted(.currency(code: "USD").locale(Locale(identifier: "en_US")))
// Force a date format for an API
let apiDate = date.formatted(.iso8601) // Prefer ISO 8601 for APIs
// Force German format for a German-language PDF export
let deDate = date.formatted(.dateTime.month(.wide).day().year().locale(Locale(identifier: "de_DE")))Warning: Never force a locale for user-facing UI. Always let the system locale drive user-visible formatting.
SwiftUI respects layoutDirection from the environment. When the user's language is RTL:
.leading = right side, .trailing = left side.padding(.leading, 16) applies to right side// Directional images SHOULD flip
Image(systemName: "chevron.forward")
.flipsForRightToLeftLayoutDirection(true)
Image(systemName: "arrow.right")
.flipsForRightToLeftLayoutDirection(true)
Image("progress-arrow")
.flipsForRightToLeftLayoutDirection(true)
// These should NOT flip:
// - Logos and brand marks
// - Photos and illustrations
// - Clock faces (clockwise is universal)
// - Music notation
// - Checkmarks
// - Mathematical symbols (+, -, =)
// - Media playback controls (play triangle always points right)
// SF Symbols with .rtl variant auto-flip (e.g., text.alignleft has text.alignright)
// Check SF Symbols app for RTL variants// Preview with RTL
#Preview("Arabic RTL") {
ContentView()
.environment(\.layoutDirection, .rightToLeft)
.environment(\.locale, Locale(identifier: "ar"))
}
// Preview with both directions side by side
#Preview("LTR vs RTL") {
HStack(spacing: 0) {
ContentView()
.environment(\.layoutDirection, .leftToRight)
.frame(maxWidth: .infinity)
Divider()
ContentView()
.environment(\.layoutDirection, .rightToLeft)
.environment(\.locale, Locale(identifier: "ar"))
.frame(maxWidth: .infinity)
}
}When mixing UIKit views via UIViewRepresentable, set semantic content attribute:
class MyUIView: UIView {
override var semanticContentAttribute: UISemanticContentAttribute {
// .forceLeftToRight for phone numbers, code
// .forceRightToLeft to force RTL
// .unspecified to follow system (default)
.unspecified
}
}When mixing LTR and RTL text (e.g., English brand names in Arabic text), Unicode bidirectional algorithm handles it automatically. For edge cases:
// Force LTR for specific content within RTL context
Text("\u{200E}+1 (555) 123-4567") // LTR mark before phone number
// Or use environment override on a specific view
Text(phoneNumber)
.environment(\.layoutDirection, .leftToRight)| Issue | Wrong | Correct |
|---|---|---|
| Fixed position | .padding(.left, 16) | .padding(.leading, 16) |
| Absolute offset | .offset(x: -20) for "move left" | Use alignment or .padding(.trailing) |
| Text alignment | .multilineTextAlignment(.left) | .multilineTextAlignment(.leading) |
| Corner radius | Only rounding top-left/top-right | Round leading/trailing corners |
| Swipe gestures | "Swipe right to delete" | "Swipe to leading edge" -- or use system gestures |
@ScaledMetric for Dynamic TypeUse @ScaledMetric to make custom spacing, icon sizes, and padding scale with the user's Dynamic Type setting.
struct ProfileRow: View {
@ScaledMetric(relativeTo: .body) private var avatarSize = 44.0
@ScaledMetric(relativeTo: .body) private var spacing = 12.0
var body: some View {
HStack(spacing: spacing) {
AvatarView()
.frame(width: avatarSize, height: avatarSize)
VStack(alignment: .leading) {
Text(name).font(.headline)
Text(subtitle).font(.subheadline)
}
}
}
}@ScaledMetric scales proportionally to a text style. Choose the text style that the metric logically accompanies:
| Text style | Base size | Use for |
|---|---|---|
.body | 17pt | General spacing, icons next to body text |
.caption | 12pt | Small icons, fine spacing |
.title | 28pt | Large icons, hero spacing |
.largeTitle | 34pt | Hero images, splash elements |
// Preview with large text
#Preview("Accessibility XXL") {
ContentView()
.dynamicTypeSize(.accessibility3)
}
// Preview matrix
#Preview("Dynamic Type Sizes") {
ScrollView {
ForEach(DynamicTypeSize.allCases, id: \.self) { size in
ContentView()
.dynamicTypeSize(size)
.padding()
.border(Color.gray)
}
}
}Accessibility Inspector (Xcode > Open Developer Tool > Accessibility Inspector) provides:
| Data type | FormatStyle | Example output (US) |
|---|---|---|
Date | .dateTime.month().day().year() | "Jan 15, 2026" |
Date range | (start..<end).formatted(date:time:) | "Jan 15 - 20, 2026" |
Date relative | .relative(presentation: .named) | "yesterday" |
Int | .number | "1,234,567" |
Int ordinal | .number.notation(.ordinal) | "3rd" |
Int compact | .number.notation(.compactName) | "1.2M" |
Double | .number.precision(.fractionLength(2)) | "3.14" |
Double | .percent | "85.6%" |
Decimal | .currency(code: "USD") | "$29.99" |
Measurement | .measurement(width: .abbreviated) | "5 km" / "3.1 mi" |
Duration | .time(pattern: .hourMinuteSecond) | "1:01:01" |
Duration | .units(width: .abbreviated) | "1 hr, 1 min" |
PersonNameComponents | .name(style: .medium) | "John Doe" |
Int64 (bytes) | .byteCount(style: .file) | "1.5 MB" |
[String] | .list(type: .and) | "A, B, and C" |
skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references