Skip to content
ExponaBLOG
SWIFT, SWIFTUI & IOS APPS · DAY 099 / 100

Dates and formatting in Swift for Swift & iOS developers

Display dates correctly.

Expona Admin7 min read

Dates and formatting in Swift for Swift & iOS developers

Date handling is one of those tasks that feels simple until it isn't. You'll inevitably encounter edge cases: a user in Tokyo seeing a different date than one in London, timestamps that mysteriously shift by an hour when you move between screens, or dates that format beautifully on the simulator but look wrong on a real device. Getting dates and formatting right in Swift requires understanding how the system manages time zones, how to perform calendar-based calculations, and which formatting approach matches your use case.

Over 99 days, we've built features across production apps. On day 99, let's tackle something that touches nearly every feature: working with dates correctly. We'll cover practical patterns we've used in e-commerce checkouts, travel apps tracking booking windows, and SaaS dashboards showing activity timelines.

Date.FormatStyle for readable output

Before iOS 15, date formatting meant wrestling with DateFormatter—a powerful but verbose API. Apple introduced Date.FormatStyle as a modern, type-safe alternative. This is what you should reach for first in any new code.

Let's say you're building a receipt screen in an e-commerce app. You need to display when the order was placed, in a format your users expect.

swift
import Foundation

let orderDate = Date()

// Basic formatted output
let formatted = orderDate.formatted()
print(formatted) // "12/15/2024, 2:30 PM"

// With a specific style
let dateOnly = orderDate.formatted(date: .abbreviated, time: .omitted)
print(dateOnly) // "12/15/2024"

let timeOnly = orderDate.formatted(date: .omitted, time: .shortened)
print(timeOnly) // "2:30 PM"

// Custom format using format strings
let custom = orderDate.formatted(.iso8601.year().month().day())
print(custom) // "2024-12-15"

In SwiftUI, this becomes even cleaner. You can use the formatted() modifier directly in your Text views without explicit conversions:

swift
import SwiftUI

struct OrderReceiptView: View {
    let orderDate: Date
    
    var body: some View {
        VStack(alignment: .leading) {
            Text("Order placed:")
            Text(orderDate, style: .date)
            
            Text("At:")
            Text(orderDate, style: .time)
            
            Text("Full timestamp:")
            Text(orderDate.formatted(date: .abbreviated, time: .standard))
        }
    }
}

Date.FormatStyle respects the device's locale automatically. A German user sees "15. Dez. 2024", a French user sees "15 déc. 2024"—without any code changes on your part. This is crucial for apps used internationally.

Calendar for date math

When you need to work with dates arithmetically—adding days, finding the next Monday, calculating how many weeks until a deadline—you reach for Calendar. This is where mistakes happen easily, especially around month boundaries and leap years.

Imagine a travel app that lets users book hotels with a minimum stay. You need to calculate valid checkout dates based on a check-in date and minimum night requirement.

swift
import Foundation

let checkInDate = Date()
let minimumNights = 3

var calendar = Calendar.current

// Add days safely
if let checkOutDate = calendar.date(byAdding: .day, value: minimumNights, to: checkInDate) {
    print("Earliest checkout: \(checkOutDate.formatted(date: .abbreviated, time: .omitted))")
}

// Find components of a date
let components = calendar.dateComponents([.year, .month, .day, .weekday], from: checkInDate)
print("Month: \(components.month ?? 0), Weekday: \(components.weekday ?? 0)")

// Get the next occurrence of a specific weekday
let nextMonday = calendar.nextDate(after: checkInDate, matching: DateComponents(weekday: 2), matchingPolicy: .nextTime)
print("Next Monday: \(nextMonday?.formatted(date: .abbreviated, time: .omitted) ?? "N/A")")

// Range of dates (e.g., all dates in the current month)
let now = Date()
if let range = calendar.range(of: .month, in: .month, for: now) {
    let startOfMonth = range.lowerBound
    let endOfMonth = range.upperBound
    print("Month spans from \(startOfMonth.formatted(date: .abbreviated, time: .omitted)) to \(endOfMonth.formatted(date: .abbreviated, time: .omitted))")
}

Key point: always use Calendar.current for user-facing calculations. It respects the user's locale and calendar system (some users have non-Gregorian calendars enabled). If you need specific calendar math independent of user settings, you can specify Calendar(identifier: .gregorian), but this is rare.

Store UTC, display in local time

This is the golden rule we've learned repeatedly: store all timestamps in UTC on your backend and in your database. Convert to the user's local time zone only when displaying. This prevents timezone-related bugs from cascading through your codebase.

Here's how it works in practice. Your backend sends timestamps in ISO 8601 format with Z (UTC):

swift
import Foundation

// Incoming from API: "2024-12-15T14:30:00Z"
let utcDateString = "2024-12-15T14:30:00Z"
let formatter = ISO8601DateFormatter()
let utcDate = formatter.date(from: utcDateString)

// Store this utcDate directly in your model
// Never adjust it, never convert it in Core Data

// When displaying, convert to user's timezone
let userTimeZone = TimeZone.current
var calendar = Calendar.current
calendar.timeZone = userTimeZone

let components = calendar.dateComponents([.hour, .minute], from: utcDate ?? Date())
print("Local time: \(components.hour ?? 0):\(String(format: "%02d", components.minute ?? 0))")

// Or use FormatStyle with timezone awareness
let displayString = utcDate?.formatted(date: .abbreviated, time: .standard) ?? "Unknown"
print(displayString) // Automatically uses device's timezone

In a CRM app, your backend tracks when a customer last engaged—always stored as UTC. When a sales rep in New York views the activity timeline, they see times in Eastern. A rep in Singapore sees Singapore time. No confusion, no manual offset tracking.

When you need to explicitly work with a specific time zone (say, showing store hours for a location in a different timezone), use:

swift
var calendar = Calendar.current
if let timeZone = TimeZone(identifier: "America/New_York") {
    calendar.timeZone = timeZone
    let components = calendar.dateComponents([.hour], from: Date())
    print("Hour in New York: \(components.hour ?? 0)")
}

Relative formatting for recency

Users prefer relative timestamps for recent events. "2 hours ago" is more intuitive than "2024-12-15 at 12:30 PM". Swift provides Date.RelativeFormatStyle for exactly this.

In a ride-hailing app, you might show when a driver last completed a ride:

swift
import Foundation

let completedRideTime = Calendar.current.date(byAdding: .hour, value: -2, to: Date())!

// Relative formatting
let relativeFormatted = completedRideTime.formatted(.relative(presentation: .named))
print(relativeFormatted) // "2 hours ago"

// With different presentations
let numeric = completedRideTime.formatted(.relative(presentation: .numeric))
print(numeric) // "2 hours ago" (similar, but always shows number)

// You can also use the older approach with DateComponentsFormatter for iOS 14 compatibility
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
formatter.allowedUnits = [.day, .hour, .minute]
if let formatted = formatter.string(from: completedRideTime, to: Date()) {
    print(formatted) // "2h" or "2 hours"
}

Be cautious with relative formatting: it's perfect for "last seen" indicators but wrong for confirmation emails ("Your order from 3 days ago is shipping today" is confusing). Use it where context is immediate and the reference point is always "now".

Common mistakes

  • Ignoring time zones entirely: A timestamp created in one timezone and parsed in another without explicit handling will silently drift. Always be explicit about whether you're working in UTC or local time.
  • Using DateFormatter instead of FormatStyle in new code: You'll find legacy code using DateFormatter. It still works, but FormatStyle is clearer and avoids many footguns. Don't copy the old pattern.
  • Assuming calendar math works the same everywhere: Adding 1 month to January 31st is ambiguous (February doesn't have 31 days). Calendar` handles this, but you need to check the result. Don't assume date(byAdding:) always succeeds.
  • Forgetting locale in formatted output: If you hardcode date formats as strings ("dd/MM/yyyy"), they won't adapt to users in other regions. Use FormatStyle instead and let the system handle localization.
  • Modifying stored dates for display: Don't convert your stored UTC timestamp to local time and save that back. Keep the UTC version as your source of truth and only convert when presenting to the user.

Key takeaways

  • Use Date.FormatStyle for displaying dates in new code—it's type-safe, locale-aware, and readable.
  • Reach for Calendar when you need date arithmetic, and always check the result since operations like adding months can be ambiguous.
  • Store all timestamps in UTC, never in local time. Convert to local time only when displaying to users.
  • Apply relative formatting ("2 hours ago") for recent events where "now" is the natural reference point.
  • Let the system handle timezone and locale conversions automatically—don't hardcode offsets or format strings.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles