How to display date and time in SwiftUI

Displaying dates or times is a very common requirement for many apps, often using a specific date formatter. Let’s see what SwiftUI brings to the table to make it easier for developers.

Coming from UIKit, if I want to display a date, my code would look like something like this.

import Foundation
import UIKit

let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .none

let label = UILabel()
label.text = dateFormatter.string(from: Date()) // "January 14, 2021"

With SwiftUI, Apple introduced a DateStyle component directly within Text view to make it more straightforward and avoid the need of creating a formatter.

struct MyView: View {
    var body: some View {
        Text(Date(), style: .date) // "January 14, 2021"
    }
}

The advantage of SwiftUI here is to be able to quickly test other region and language with environment variable.

import SwiftUI

struct MyView: View {
    var body: some View {
        Text(Date(), style: .date) // "14 janvier 2021"
    }
}

struct MyView_Previews: PreviewProvider {
    static var previews: some View {
        MyView()
            .environment(\.locale, Locale(identifier: "fr"))
    }
}

DateStyle comes with a suite of predefined format to display date, time, offset of time and more.

However, it also has its own limitation. For instance, there is no predefined format to display date and time within one Text view.

To work around, the naive way could be stack different Text views together, like following.

import SwiftUI

struct MyView: View {
    let date = Date()

    var body: some View {
        HStack {
            Text(date, style: .date)
            Text(date, style: .time)
        } // January 14, 2021 10:00 AM
    }
}

However, this is not as elegant as our previous DateFormatter. For instance using dateStyle and timeStyle combine makes a more readable format.

let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .short

let label = UILabel()
dateFormatter.string(from: Date()) // "January 14, 2021 at 10:00 AM"

Fortunately for us, if the date style within Text view doesn’t support this type of combination, we can still inject our own formatter within the view as long as the subject is a NSObject or ReferenceConvertible.

Date type is one of those, so we can take advantage of it.

import SwiftUI

struct MyView: View {
    let date: Date
    let dateFormatter: DateFormatter
    
    init() {
        date = Date()
        dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .long
        dateFormatter.timeStyle = .short
    }
    
    var body: some View {
        Text(date, formatter: dateFormatter) // "January 14, 2021 at 10:00 AM"
    }
}

Awesome! Now we’re back on track, and Xcode Preview with environment variable is still reflecting the latest values.

import SwiftUI

struct MyView: View {
    let date: Date
    let dateFormatter: DateFormatter
    
    init() {
        date = Date()
        dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .long
        dateFormatter.timeStyle = .short
    }
    
    var body: some View {
        Text(date, formatter: dateFormatter) // "14 janvier 2021 Γ  10:00"
    }
}

struct MyView_Previews: PreviewProvider {
    static var previews: some View {
        MyView()
            .environment(\.locale, Locale(identifier: "fr"))
    }
}

What if I want to use a String value in my Text view instead and don’t want to expose the formatter within the body?

Well, this works also fine if you launch the app, but you’ll notice that Xcode Preview won’t apply anymore the environment variable to computed properties the same way.

import SwiftUI

struct MyView: View {
    let date: Date
    let dateFormatter: DateFormatter
    
    init() {
        date = Date()
        dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .long
        dateFormatter.timeStyle = .short
    }
    
    var dateValue: String {
        return dateFormatter.string(from: date)
    }
    
    var body: some View {
        Text(dateValue) // "January 14, 2021 at 10:00 AM
    }
}

struct MyView_Previews: PreviewProvider {
    static var previews: some View {
        MyView()
            .environment(\.locale, Locale(identifier: "fr"))
    }
}

It seems the environment variable isn’t enforced to the date formatter, which doesn’t allow me to preview my code for french region anymore. However, it’s only a preview problem, an app launched would work fine.

If we still want to keep our computed property AND Xcode Preview enforcing the format, we’ll need extra work to make it work. One way is using abstraction.

First part is to create a protocol to abstract Foundation’s date formatter and create a preview date formatter to enforce our locale component.

protocol DateFormatterProtocol {
    var dateStyle: DateFormatter.Style { get set }
    var timeStyle: DateFormatter.Style { get set }
    func string(from date: Date) -> String
}

extension DateFormatter: DateFormatterProtocol { }

struct PreviewDateFormatter: DateFormatterProtocol {
    
    let dateFormatter: DateFormatter 
    
    init(locale: Locale) {
        dateFormatter = DateFormatter()
        dateFormatter.locale = locale
    }
    
    var dateStyle: DateFormatter.Style {
        get {
            dateFormatter.dateStyle
        }
        set {
            dateFormatter.dateStyle = newValue
        }
    }
    
    var timeStyle: DateFormatter.Style{
        get {
            dateFormatter.timeStyle
        }
        set {
            dateFormatter.timeStyle = newValue
        }
    }
    
    func string(from date: Date) -> String {
        return dateFormatter.string(from: date)
    }
    
}

Then we can update our view accordingly, injecting our new PreviewDateFormatter only for Xcode Preview.

struct MyView: View {
    let date: Date
    var dateFormatter: DateFormatterProtocol
    
    init(dateFormatter: DateFormatterProtocol = DateFormatter()) {
        date = Date()
        self.dateFormatter = dateFormatter
        self.dateFormatter.dateStyle = .long
        self.dateFormatter.timeStyle = .short
    }
    
    var dateValue: String {
        return dateFormatter.string(from: date)
    }
    
    var body: some View {
        Text(dateValue) // "14 janvier 2021 Γ  10:00"
    }
}

struct MyView_Previews: PreviewProvider {
    static var previews: some View {
        MyView(dateFormatter: PreviewDateFormatter(locale: Locale(identifier: "fr")))
    }
}

If this solution works, it requires us to do extra steps and is hardly scalable across the project: any formatter (length, currency, name, etc) would require a new abstraction.


Overall, SwiftUI brings a lot of simple options that can be a great start to display date and time into an iOS app. We also still have the possibility to go further and use a custom date formatter for a nicer experience.

If there are some limitations, we managed to work around, but let’s make sure to consider pros and cons when coming to abstraction. It is more flexible but require more work and code to make sure to work across the app.

© 2021 Benoit Pasquier. All Rights Reserved
Author's picture

Benoit Pasquier

iOS Software engineer πŸ‡«πŸ‡·, writing about Swift, Data and more.

ShopBack πŸ’°

Singapore πŸ‡ΈπŸ‡¬