October 18, 2019
In the wild world of 2019 SwiftUI development, lots of things aren't documented. One such thing I ran into recently was the usage of RelativeDateTimeFormatter
in a Text
view.
RelativeDateTimeFormatter
is a new formatter which is available in iOS 13+ and macOS 10.15+. Though it's not documented at the time of writing (just like 14.6% of Foundation), there have been a fair amount of folks online writing about it. This formatter formats dates relative each other as well as relative DateComponents
- its output looks like "one minute ago" or "two minutes from now".
Most blog posts about RelativeDateTimeFormatter
show its usage like this:
let formatter = RelativeDateTimeFormatter()
formatter.localizedString(from: DateComponents(day: -1)) // "1 day ago"
SwiftUI declares a custom string interpolation (which is a new feature in Swift 5) called LocalizedStringKey.StringInterpolation
(also undocumented at the time of writing, like 59.4% of SwiftUI) which allows you to write Text
views with formatters like so:
Text("My String \(myVariable, formatter: myFormatter)")
Let's assume I want to show the relative time from the Unix Epoch until now. I'll just write a Text
the same way, right?
struct ContentView: View {
static let formatter = RelativeDateTimeFormatter()
var body: some View {
let unixEpoch = Date(timeIntervalSince1970: 0)
let components = Calendar.current.dateComponents(
[.day, .year, .month, .minute, .second],
from: Date(),
to: unixEpoch
)
return VStack {
Text("Current date is:")
Text("\(components, formatter: Self.formatter)").bold()
Text("since the unix Epoch")
Spacer()
}
}
}
Unfortunately this doesn't work - the middle text doesn't show up:
There's also an error in the console!
The supplied formatter <NSRelativeDateTimeFormatter: 0x6000025c7c00>
returned `nil` when invoked with <NSDateComponents: 0x6000026b4450> {
Calendar Year: -49
Month: -9
Day: -18
Minute: -1017
Second: -43. An empty string will be used instead.
As far as I can tell, the reason this happens is because RelativeDateTimeFormatter
has a few different methods, with different signatures, for formatting its relative dates:
func localizedString(for: Date, relativeTo: Date) -> String
func localizedString(from: DateComponents) -> String
func localizedString(fromTimeInterval: TimeInterval) -> String
func string(for: Any?) -> String?
Only one of those matches the Formatter superclass definition that SwiftUI's string interpolation uses. I'm guessing that RelativeDateTimeFormatter
's string(for:)
method doesn't call into the same code that localizedString(from:)
uses - it seems like string(for:)
handles the Date
case, but not the DateComponents
case.
Hopefully this changes in a future version of iOS, but for now, we can solve the problem in a couple of ways:
Since what we want is the date relative to right now, and that's the default behavior of RelativeDateTimeFormatter
, we can just pass in epochTime
:
struct ContentView: View {
static let formatter = RelativeDateTimeFormatter()
var body: some View {
let unixEpoch = Date(timeIntervalSince1970: 0)
return VStack {
Text("Current date is:")
Text("\(unixEpoch, formatter: Self.formatter)").bold()
Text("since the unix Epoch")
Spacer()
}
}
}
This gets us the result we want:
Also note: even though nothing at the surface level is calling localizedString
, the string will be properly localized as long as you set the formatter's locale
.
We can also ignore the custom string interpolation, and just call the formatter's localizedString
:
struct ContentView: View {
static let formatter = RelativeDateTimeFormatter()
var body: some View {
let unixEpoch = Date(timeIntervalSince1970: 0)
let components = Calendar.current.dateComponents(
[.day, .year, .month, .minute, .second],
from: Date(),
to: unixEpoch
)
return VStack {
Text("Current date is:")
Text("\(Self.formatter.localizedString(from: components))").bold()
Text("since the unix Epoch")
Spacer()
}
}
}
Either of the solutions above would work for this specific issue, though if you want to output the relative time between two dates (instead of between one date and now) with the formatter:
string interpolation, looks like you're out of luck.
Sample code for this post is available on GitHub .
I'm Noah, a software developer based in the San Francisco Bay Area. I focus mainly on full stack web and iOS development