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 myText
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.