January 8, 2025
How to Format Dates and Times with SystemFormatStyle in SwiftUI
Learn to Build Accurate Counting-Up Timers in SwiftUI with Real-Time Updates
Alexander Adelmaer
3 min read
Apple has revolutionized time formatting in SwiftUI by introducing SystemFormatStyle, a flexible and powerful API for handling date, time, and interval-related formatting. It simplifies tasks like displaying relative time differences, precise offsets, or timers, enabling developers to focus on building rich user experiences.
This guide provides a deep dive into DateReference, DateOffset, Stopwatch, Timer, and TimeDataSource.
What is SystemFormatStyle?
SystemFormatStyle is a declarative way to render time-related data in SwiftUI. Its modularity allows for precise formatting while integrating with modern SwiftUI views, ensuring seamless localization and user-friendly displays.
Let's explore each component with multiple practical examples.
1. DateReference: Natural Relative Time Formatting
The DateReference style is designed to express relative differences between dates in the most natural way, like "3 days ago" or "Next year."
Example 1: Simple Relative Time Difference
struct DateReferenceSimpleExample: View {
let futureDate = Calendar.current.date(byAdding: .day, value: 10, to: Date())!
var body: some View {
Text(Date.now, format: .reference(to: futureDate))
.font(.headline)
.padding()
}
}struct DateReferenceSimpleExample: View {
let futureDate = Calendar.current.date(byAdding: .day, value: 10, to: Date())!
var body: some View {
Text(Date.now, format: .reference(to: futureDate))
.font(.headline)
.padding()
}
}Displays: "In 10 Days"
Example 2: Limiting Fields in Relative Time
struct DateReferenceLimitedFields: View {
let eventDate = Calendar.current.date(byAdding: .year, value: 1, to: Date())!
var body: some View {
Text(Date.now, format: .reference(to: eventDate, allowedFields: [.year, .month], maxFieldCount: 1))
.font(.headline)
.padding()
}
}struct DateReferenceLimitedFields: View {
let eventDate = Calendar.current.date(byAdding: .year, value: 1, to: Date())!
var body: some View {
Text(Date.now, format: .reference(to: eventDate, allowedFields: [.year, .month], maxFieldCount: 1))
.font(.headline)
.padding()
}
}Displays: "In 1 year."
Example 3: Setting a Threshold for Switching to Absolute Time
struct DateReferenceWithThreshold: View {
let distantDate = Calendar.current.date(byAdding: .year, value: 2, to: Date())!
var body: some View {
Text(Date.now, format: .reference(to: distantDate, thresholdField: .year))
.font(.headline)
.padding()
}
}struct DateReferenceWithThreshold: View {
let distantDate = Calendar.current.date(byAdding: .year, value: 2, to: Date())!
var body: some View {
Text(Date.now, format: .reference(to: distantDate, thresholdField: .year))
.font(.headline)
.padding()
}
}Displays: "2027" (switches to absolute after the threshold).
2. DateOffset: Precise Time Intervals
The DateOffset style is perfect for displaying exact intervals between two dates with full control over units like years, months, days, and more.
Example 1: Showing Days and Hours Difference
struct DateOffsetDaysHours: View {
let pastDate = Calendar.current.date(byAdding: .hour, value: -50, to: Date())!
var body: some View {
Text(Date.now, format: .offset(to: pastDate, allowedFields: [.day, .hour], maxFieldCount: 2))
.font(.headline)
.padding()
}
}struct DateOffsetDaysHours: View {
let pastDate = Calendar.current.date(byAdding: .hour, value: -50, to: Date())!
var body: some View {
Text(Date.now, format: .offset(to: pastDate, allowedFields: [.day, .hour], maxFieldCount: 2))
.font(.headline)
.padding()
}
}Displays: "2 days, 2 hours"
Example 2: Only Seconds Difference
struct DateOffsetSeconds: View {
let futureDate = Calendar.current.date(byAdding: .second, value: 120, to: Date())!
var body: some View {
Text(Date.now, format: .offset(to: futureDate, allowedFields: [.second]))
.font(.headline)
.padding()
}
}struct DateOffsetSeconds: View {
let futureDate = Calendar.current.date(byAdding: .second, value: 120, to: Date())!
var body: some View {
Text(Date.now, format: .offset(to: futureDate, allowedFields: [.second]))
.font(.headline)
.padding()
}
}Displays: "-120 Seconds"
Example 3: Controlling the Sign of the Interval
struct DateOffsetWithSign: View {
let pastDate = Calendar.current.date(byAdding: .day, value: -5, to: Date())!
var body: some View {
Text(Date.now, format: .offset(to: pastDate, sign: .always))
.font(.headline)
.padding()
}
}struct DateOffsetWithSign: View {
let pastDate = Calendar.current.date(byAdding: .day, value: -5, to: Date())!
var body: some View {
Text(Date.now, format: .offset(to: pastDate, sign: .always))
.font(.headline)
.padding()
}
}Displays: "+5 days."
3. Stopwatch: Measuring Elapsed Time
The Stopwatch style is ideal for tracking elapsed time in a format similar to a stopwatch.
struct TimerStopwatchView: View {
let now: TimeDataSource<Date> = .currentDate
@State private var timerStart: Date?
@State private var timerEnd: Date?
@State private var stopwatchStart: Date?
var body: some View {
VStack(spacing: 40) {
// Live Digital Clock
Text(now, format: .dateTime.hour().minute().second())
.font(.system(size: 50, weight: .bold, design: .monospaced))
.padding(.bottom, 20)
// Timer with Countdown
VStack(spacing: 10) {
Button("Set 15-Second Timer") {
timerStart = Date()
timerEnd = Date().addingTimeInterval(15)
}
if let timerStart, let timerEnd {
Text(now, format: .timer(countingDownIn: timerStart..<timerEnd))
.font(.system(size: 40, weight: .bold, design: .monospaced))
.foregroundColor(.red)
}
}
// Stopwatch
VStack(spacing: 10) {
Button("Start Stopwatch") {
stopwatchStart = Date()
}
if let stopwatchStart {
Text(now, format: .stopwatch(startingAt: stopwatchStart, showsHours: true, maxFieldCount: 3, maxPrecision: .milliseconds(1)))
.font(.system(size: 40, weight: .bold, design: .monospaced))
.foregroundColor(.blue)
}
}
}
.padding()
}
}struct TimerStopwatchView: View {
let now: TimeDataSource<Date> = .currentDate
@State private var timerStart: Date?
@State private var timerEnd: Date?
@State private var stopwatchStart: Date?
var body: some View {
VStack(spacing: 40) {
// Live Digital Clock
Text(now, format: .dateTime.hour().minute().second())
.font(.system(size: 50, weight: .bold, design: .monospaced))
.padding(.bottom, 20)
// Timer with Countdown
VStack(spacing: 10) {
Button("Set 15-Second Timer") {
timerStart = Date()
timerEnd = Date().addingTimeInterval(15)
}
if let timerStart, let timerEnd {
Text(now, format: .timer(countingDownIn: timerStart..<timerEnd))
.font(.system(size: 40, weight: .bold, design: .monospaced))
.foregroundColor(.red)
}
}
// Stopwatch
VStack(spacing: 10) {
Button("Start Stopwatch") {
stopwatchStart = Date()
}
if let stopwatchStart {
Text(now, format: .stopwatch(startingAt: stopwatchStart, showsHours: true, maxFieldCount: 3, maxPrecision: .milliseconds(1)))
.font(.system(size: 40, weight: .bold, design: .monospaced))
.foregroundColor(.blue)
}
}
}
.padding()
}
}4. Timer: Counting Up or Down
The Timer style is used to represent time intervals dynamically, either counting up or down.
struct TimerCountdown: View {
let now: TimeDataSource<Date> = .currentDate
let futureDate = Date().addingTimeInterval(120) // 2 minutes from now
var body: some View {
VStack(spacing: 20) {
// Countdown Timer
if Date() < futureDate {
Text(now, format: .timer(countingDownIn: Date()..<futureDate))
.font(.system(size: 50, weight: .bold, design: .monospaced))
.monospacedDigit()
} else {
// Display completion message
Text("Countdown Complete")
.font(.headline)
.foregroundColor(.green)
}
}
.padding()
}
}struct TimerCountdown: View {
let now: TimeDataSource<Date> = .currentDate
let futureDate = Date().addingTimeInterval(120) // 2 minutes from now
var body: some View {
VStack(spacing: 20) {
// Countdown Timer
if Date() < futureDate {
Text(now, format: .timer(countingDownIn: Date()..<futureDate))
.font(.system(size: 50, weight: .bold, design: .monospaced))
.monospacedDigit()
} else {
// Display completion message
Text("Countdown Complete")
.font(.headline)
.foregroundColor(.green)
}
}
.padding()
}
}Displays: "01:59."
5. TimeDataSource: Live Data Updates
The TimeDataSource style provides real-time updates for views requiring dynamic time data.
struct TimeDataSourceClock: View {
let now: TimeDataSource<Date> = .currentDate
var body: some View {
Text(now, format: .dateTime.hour().minute().second())
.font(.largeTitle)
.monospacedDigit()
.padding()
}
}struct TimeDataSourceClock: View {
let now: TimeDataSource<Date> = .currentDate
var body: some View {
Text(now, format: .dateTime.hour().minute().second())
.font(.largeTitle)
.monospacedDigit()
.padding()
}
}Displays: "10:23:45."
Best Practices for SystemFormatStyle
- Choose Fields Wisely: Avoid cluttering UI with unnecessary units.
- Leverage Localization: The API automatically adjusts to user locales.
- Test for Edge Cases: Handle leap years, daylight saving time, and time zones.
- Combine Styles: Use a mix of DateReference and DateOffset for contextual time displays.
Thanks for Reading
๐๐ If you enjoyed and found this post useful, please clap and share it.
You Might Be Interested In:
- SwiftUI Menu Tutorial โ How to use Menu in SwiftUI
- SwiftUI Grid: Better Layouts with Dynamic Grids
- SwiftUI Navigation Stack
Learn iOS Development & Improve your Skills
๐ท Latest SwiftUI Tutorials ๐ SwiftUI Handbook
Join for Updates
๐ Join AppMakers on Medium for SwiftUI, Swift & iOS App Development๐ฉ Subscribe to our Mailing List
๐ Follow the Editor
Happy Coding to You ๐ง๐ฝโ๐ป๐ฉ๐ปโ๐ป