In Bite #286, we looked at the basics of using DateFormatter
s to transform Date
s into String
s.
Today we'll explore another common use case of Date
s in our apps: "relative" date strings.
Think "2 days ago", "10 minutes ago", "Yesterday", and "Just now". Representing timestamps this way has become common, and it can help our users more quickly understand how old a piece of content is.
Let's add this functionality to our app by extending Date
to give it a new computed property to do this.
We'll begin by adding a new File to our Xcode Project. We'll call it Date+Relative.swift
. (The Type+SomeExtensionName
convention is borrowed from Objective-C.
Then, we'll set up our extension:
extension Date {
var relativelyFormatted: String {
// TODO
}
}
Nice. Next, we'll need a way to calculate how much time is in between now, and self
(from the extended Date
's point of view). We could do a bunch of ugly math, but we've got a better way!
This is a fantastic use case for Foundation's DateComponents
type. It has a function that can calculate the difference between two Date
s, and provide it to us in neatly (pre-calculated) components like days, months, hours, seconds, etc.
We'll begin our implementation like so:
let now = Date()
let components = Calendar.current.dateComponents(
[.year, .month, .weekOfYear, .day, .hour, .minute, .second],
from: now,
to: self
)
We ask the current Calendar
to please calculate the quantity of each of the components we passed in for the time between the Date
we passed in as the from
argument to the Date
we passed into the to
argument.
Beautiful. Now, the returned DateComponents
type has a bunch of properties we'll use to build our relatively formatted date `String.
Finally, we'll inspect each date component (starting with most broad at the top) and return a nicely formatted String
:
if let years = components.year, years > 0 {
return "\(years) year\(years == 1 ? "" : "s") ago"
}
if let months = components.month, months > 0 {
return "\(months) month\(months == 1 ? "" : "s") ago"
}
if let weeks = components.weekOfYear, weeks > 0 {
return "\(weeks) week\(weeks == 1 ? "" : "s") ago"
}
if let days = components.day, days > 0 {
guard days > 1 else { return "yesterday" }
return "\(days) day\(days == 1 ? "" : "s") ago"
}
if let hours = components.hour, hours > 0 {
return "\(hours) hour\(hours == 1 ? "" : "s") ago"
}
if let minutes = components.minute, minutes > 0 {
return "\(minutes) minute\(minutes == 1 ? "" : "s") ago"
}
if let seconds = components.second, seconds > 30 {
return "\(seconds) second\(seconds == 1 ? "" : "s") ago"
}
return "just now"
Doing things manually like this allows us to completely customize and control exactly how this relative date strings appear in our app.
This is fairly straightforward. We get a little fancy for the "days" component, allowing for "yesterday"
. We could also easily add support for future dates (i.e. "tomorrow"
) here as well.
All that's left to do is test it out. We can do that easily by constructing some Date
s in the past, then print
ing our new property:
Date(timeIntervalSinceNow: 1).relativelyFormatted
// "just now"
Date(timeIntervalSinceNow: 12).relativelyFormatted
// "just now"
Date(timeIntervalSinceNow: 58).relativelyFormatted
// "58 seconds ago"
Date(timeIntervalSinceNow: 123).relativelyFormatted
// "2 minutes ago"
Date(timeIntervalSinceNow: 1234).relativelyFormatted
// "20 minutes ago"
Date(timeIntervalSinceNow: 12345).relativelyFormatted
// "3 hours ago"
Date(timeIntervalSinceNow: 123456).relativelyFormatted
// "yesterday"
Date(timeIntervalSinceNow: 1234567).relativelyFormatted
// "2 weeks ago"
Date(timeIntervalSinceNow: 12345689).relativelyFormatted
// "2 months ago"
Date(timeIntervalSinceNow: 123456890).relativelyFormatted
// "3 years ago"
Date(timeIntervalSinceNow: 1234568901)
.relativelyFormatted // "39 years ago"
Date(timeIntervalSinceNow: 12345689012)
.relativelyFormatted // "391 years ago"