The Real Developer's Guide to Formatters
Look, we’ve all been there - staring at a timestamp like “1703761234” and trying to figure out how to show it as “Dec 28, 2023” without writing a mess of string manipulation code. Or dealing with that one PM who insists that file sizes should show as “2.1 GB” instead of raw bytes.
Here’s what I’ve learned after years of wrestling with formatters:
Date Formatting: Your New Best Friend
The DateFormatter is probably what brought you here. Yeah, you could split strings and do math, but please don’t - I’ve seen that code in production and it’s not pretty.
let formatter = DateFormatter()
formatter.dateFormat = "MMM d, yyyy"
let readable = formatter.string(from: someDate) // "Dec 28, 2023"
Pro tip: Keep this formatter around! Creating new ones is surprisingly expensive. I learned this the hard way when our scroll performance tanked because we were making a new formatter for every table cell. Don’t be like past-me.
File Sizes Without The Pain
Remember calculating megabytes by dividing bytes by 1024 three times? ByteCountFormatter has your back:
let formatter = ByteCountFormatter()
formatter.countStyle = .binary // Uses real computer math
let size = formatter.string(fromByteCount: 1_073_741_824) // "1 GB"
No more “is it 1000 or 1024?” debates with your team. The formatter handles it all.
The Absolute Life-Saver: ListFormatter
This one’s newer but trust me, you want it. Ever had to join an array of strings with commas and “and”? And then localize it? And then your QA team tests it in Arabic?
let items = ["coffee", "tea", "water"]
ListFormatter.localizedString(byJoining: items) // "coffee, tea, and water"
That’s it. It handles Oxford commas, different languages, everything. It’s beautiful.
we can use it to format lists of dates:
let listFormatter = ListFormatter()
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .short
listFormatter.itemFormatter = dateFormatter
// Now it can format lists of dates!
Numbers: Where Everything Can Go Wrong
Ever had a European user complain that your prices are wrong because you assumed everyone uses periods for decimals? Yeah, NumberFormatter is your friend here:
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = Locale(identifier: "de_DE")
formatter.string(from: 1234.56) // "1.234,56 €"
NumberFormatter’s Hidden Gems
let formatter = NumberFormatter()
// For Indian numbering system (1,100,000 instead of 100,000)
formatter.numberStyle = .decimal
formatter.locale = Locale(identifier: "en_IN")
// For spellouts
formatter.numberStyle = .spellOut
formatter.string(from: 1234) // "one thousand two hundred thirty-four"
// For ordinals with gender (Spanish)
formatter.numberStyle = .ordinal
formatter.locale = Locale(identifier: "es")
Person Names: Culture Is Hard
This one’s saved my bacon multiple times. Japanese names? Chinese names? Russian patronymics? PersonNameComponentsFormatter handles it all:
let formatter = PersonNameComponentsFormatter()
var name = PersonNameComponents()
name.familyName = "Yamamoto"
name.givenName = "Kenji"
// It'll show this correctly based on locale
formatter.string(from: name)
True story: We had a bug in our conference app where Japanese attendee names were showing up backwards. This fixed it in like 3 lines of code.
Relative Dates: The Social Media Problem
You know those “Posted 5 minutes ago” labels? There’s actually a formatter for that:
let formatter = RelativeDateTimeFormatter()
let posted = Date().addingTimeInterval(-3600)
formatter.localizedString(for: posted, relativeTo: Date()) // "1 hour ago"
DateComponentsFormatter
For those “2 hours 15 minutes remaining” messages:
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute]
formatter.string(from: 8100) // "2 hours, 15 minutes"
DateIntervalFormatter
Show time ranges without the headache:
let formatter = DateIntervalFormatter()
formatter.string(from: startDate, to: endDate) // "10:00 AM - 2:00 PM"
ISO8601DateFormatter
For dealing with those annoying API dates:
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime]
let date = formatter.date(from: "2024-12-15T14:30:00Z")
CompactDateFormatter
New in iOS 15, for when space is tight:
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.doesRelativeDateFormatting = true
// Shows stuff like "Today" or "Yesterday" instead of dates
Measurement Hell: The Other Formatters
MeasurementFormatter
For when your fitness app users argue about miles vs kilometers:
let distance = Measurement(value: 5.0, unit: UnitLength.kilometers)
let formatter = MeasurementFormatter()
formatter.string(from: distance) // "5 km" (or "3.1 mi" for US folks)
EnergyFormatter
Perfect for calorie counting apps:
let formatter = EnergyFormatter()
formatter.string(fromValue: 500, unit: .calorie) // "500 cal"
formatter.string(fromValue: 2.1, unit: .kilojoule) // "2.1 kJ"
LengthFormatter
Height and distance that actually makes sense:
let formatter = LengthFormatter()
formatter.string(fromMeters: 1.83) // "6 ft" in US, "1.83 m" elsewhere
MassFormatter
Weight without the conversion headaches:
let formatter = MassFormatter()
formatter.string(fromKilograms: 75) // "165 lb" in US, "75 kg" elsewhere
URLComponentsFormatter
For building clean URLs:
var components = URLComponents()
components.scheme = "https"
components.host = "example.com"
components.path = "/path"
IntegerOrdinalNumberFormatter
For those “1st, 2nd, 3rd” situations:
let formatter = NumberFormatter()
formatter.numberStyle = .ordinal
formatter.string(from: 42) // "42nd"
###PersonNameComponentsFormatter (Advanced Usage) Here’s the deep stuff people don’t know about:
let formatter = PersonNameComponentsFormatter()
var name = PersonNameComponents()
name.phoneticRepresentation = PersonNameComponents()
name.phoneticRepresentation?.familyName = "ヤマモト"
name.phoneticRepresentation?.givenName = "ケンジ"
CNPostalAddressFormatter
For properly formatting addresses across countries:
let address = CNMutablePostalAddress()
address.street = "1 Infinite Loop"
address.city = "Cupertino"
CNPostalAddressFormatter().string(from: address)
ICUDateFormatter
For when you need absolute control over date formats:
// Using ICU date format patterns
let format = "EEEE, d 'of' MMMM, y 'at' HH:mm:ss"
let formatter = DateFormatter()
formatter.setLocalizedDateFormatFromTemplate(format)
Natural Language Formatting
import NaturalLanguage
let formatter = NaturalLanguageNumberFormatter()
formatter.numberStyle = .ordinal
formatter.string(from: 123) // "hundred and twenty-third"
// Language-specific formatting
formatter.locale = Locale(identifier: "fr_FR")
SwiftUI Built-in Formatters
// Date formatting
Text(Date(), style: .date)
Text(Date(), style: .timer)
Text(Date(), style: .relative)
Text(Date()..<Date().addingTimeInterval(3600), style: .timer)
// Number formatting
Text(1234.56, format: .currency(code: "USD"))
Text(0.75, format: .percent)
Text(1234.56, format: .number.precision(.fractionLength(2)))
// List formatting
Text(["apple", "banana", "orange"], format: .list(type: .and))
PersonActivityFormatter
New in iOS 15, for activity and fitness apps:
let formatter = PersonActivityFormatter()
formatter.string(forActivity: .running)
Measurement Formatting in SwiftUI
// Distance
let distance = Measurement(value: 1.5, unit: UnitLength.kilometers)
Text(distance, format: .measurement(width: .wide))
// Temperature
let temp = Measurement(value: 25, unit: UnitTemperature.celsius)
Text(temp, format: .measurement(width: .narrow))
Custom Format Styles in SwiftUI
struct CustomNumberFormat: FormatStyle {
func format(_ value: Int) -> String {
"Number: \(value)"
}
}
Text(42, format: CustomNumberFormat())
AttributedString Formatting
var attributed = AttributedString("Price: ")
attributed += AttributedString(123.45, format: .currency(code: "EUR"))
Text(attributed)
Duration Formatting in SwiftUI
// New in iOS 16
Text(Duration.seconds(3725))
Text(Duration.hours(2), format: .units(allowed: [.hours, .minutes]))
And for really specialized cases, you can even create custom formatters:
class CustomFormatter: Formatter {
override func string(for obj: Any?) -> String? {
guard let value = obj as? YourType else { return nil }
// Custom formatting logic
return formattedString
}
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?,
for string: String,
errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
// Custom parsing logic
return true
}
}
Before I knew about this, I wrote this horrible mess of if-statements comparing time intervals. The code review comments still haunt me.
I’ll improve the performance and conclusion sections to be more comprehensive and practical:
Performance: The Things That Will Bite You
Why Performance Matters
Creating formatters is expensive - we’re talking about operations that can take several milliseconds. That might not sound like much, but create a few formatters in a table view cell and watch your scrolling stutter.
The Right Way to Cache
Here’s a production-ready formatter cache implementation:
final class FormatterCache {
static let shared = FormatterCache()
private let queue = DispatchQueue(label: "com.app.formatters", attributes: .concurrent)
private var formatters: [String: Any] = [:]
// Date Formatters
func dateFormatter(format: String) -> DateFormatter {
queue.sync {
if let cached = formatters[format] as? DateFormatter {
return cached
}
let formatter = DateFormatter()
formatter.dateFormat = format
formatters[format] = formatter
return formatter
}
}
// Currency with locale
func currencyFormatter(locale: Locale) -> NumberFormatter {
let key = "currency_\(locale.identifier)"
return queue.sync {
if let cached = formatters[key] as? NumberFormatter {
return cached
}
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = locale
formatters[key] = formatter
return formatter
}
}
// Relative time
lazy var relativeTime: RelativeDateTimeFormatter = {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .full
return formatter
}()
// List formatter
lazy var list: ListFormatter = {
let formatter = ListFormatter()
return formatter
}()
}
Usage:
let price = FormatterCache.shared.currencyFormatter(locale: .current)
.string(from: 99.99)
let date = FormatterCache.shared.dateFormatter(format: "yyyy-MM-dd")
.string(from: Date())
Real-World Performance Tips
- Batch Processing: If you’re formatting many items, do it in the background:
DispatchQueue.global().async {
let formatted = items.map { formatter.string(from: $0) }
DispatchQueue.main.async {
self.updateUI(with: formatted)
}
}
- UITableView/UICollectionView Optimization:
class MyCell: UITableViewCell {
private lazy var dateFormatter = FormatterCache.shared
.dateFormatter(format: "MMM d")
func configure(with date: Date) {
dateLabel.text = dateFormatter.string(from: date)
}
}
- Avoid Redundant Formatting:
// Bad
tableView.visibleCells.forEach { cell in
cell.priceLabel.text = formatter.string(from: price)
}
// Good
let formattedPrice = formatter.string(from: price)
tableView.visibleCells.forEach { cell in
cell.priceLabel.text = formattedPrice
}
Conclusion: Lessons From The Trenches
What I Wish I Knew Earlier
- Always use formatters instead of manual string manipulation.
- Cache your formatters, but do it thread-safely.
- Test with different locales early - don’t wait for QA to find issues.
- When in doubt, check the locale’s formatting patterns.
Common Gotchas
- Number parsing can fail silently - always check for nil.
- Date formatters are timezone-sensitive.
- Some formatters aren’t thread-safe.
- Localization changes require formatter recreation.
When Things Go Wrong
// Debug helper
extension Formatter {
func debugDescription() -> String {
"""
Formatter: \(type(of: self))
Locale: \(locale?.identifier ?? "nil")
TimeZone: \((self as? DateFormatter)?.timeZone?.identifier ?? "n/a")
Format: \((self as? DateFormatter)?.dateFormat ?? "n/a")
"""
}
}
and dont forget we can listen for locale changes and update the formatters accordingly.
// Add notifications handling
NotificationCenter.default.addObserver(self, selector: #selector(clearCache), name: NSLocale.currentLocaleDidChangeNotification, object: nil)
Remember: Formatters are your friends, but like all good friends, you need to treat them with respect and understand their quirks. Happy formatting!