Weekly Swift articles, podcasts and tips by John Sundell.

Computing dates in Swift

Published on 29 Sep 2019

The task of computing various dates and timestamps is something that at first may seem deceptively simple. We all know that a minute consists of sixty seconds, an hour of sixty minutes and that there are twenty-four hours in a day — however, correctly computing and modifying dates often requires a far more complex model.

Thankfully, Apple ships a complete set of date and calendar APIs as part of Foundation, and while some of those APIs may seem a bit complex at first glance — they enable us to write date handling code that takes all sorts of anomalies and cultural differences into account, without requiring us to know about any of those conditions ourselves.

This week, let’s take a look at a few examples of how we might use those APIs — how they can enable us to easily make the way we compute dates more correct, and how we can build our own lightweight abstractions on top of them to make dealing with dates in Swift a lot easier.

Not always a time interval away

The TimeInterval type (which is actually just a type alias for Double) enables us to express time as an interval of seconds — which is incredibly useful when we want to compute a date within the very near future, such as in this example, in which we’re scheduling a notification to be shown after 20 seconds:

let date = Date().addingTimeInterval(20)
schedule(notification, for: date)

However, the above way of computing dates breaks down quite quickly as our time interval grows — as simply adding a number of seconds to the current date won’t account for things like leap seconds, changes in daylight savings time, or other time corrections and adjustments.

For example, although the following code might give us a date that’s technically 24 hours away in terms of number of seconds, it won’t always be “tomorrow” — as a time adjustment might either push the resulting date into the following or previous day, or otherwise affect the perceived time of day:

let tomorrow = Date().addingTimeInterval(60 * 60 * 24)

While absolute precision isn’t always required when handling dates, using manually computed time intervals can lead to some really tricky bugs if our code assumes that tomorrow doesn’t simply mean “24 hours away in terms of absolute time”, but rather “the same time as now, but tomorrow”.

To compute the latter in a way that’s a lot more robust, let’s instead use Foundation’s Calendar type, which enables us to manipulate dates according to a specific calendar (the most common of which is the Gregorian Calendar) — with nanosecond-level precision:

// Calendar.current gives us access to a calendar that’s
// configured according to the user’s system settings:
let calendar = Calendar.current
let date = Date()

// Define which date components that we want to be considered
// when looking for tomorrow’s date. This essentially decides
// what level of precision that we’d like:
let components = calendar.dateComponents(
    [.hour, .minute, .second, .nanosecond],
    from: date
)

let tomorrow = calendar.nextDate(
    after: date,
    matching: components,
    matchingPolicy: .nextTime
)

If we were planning to store the above Calendar value for further use, it’d be much better to access it using Calendar.autoupdatingCurrent, since that’ll track any changes that the user might make to their system-wide calendar preferences.

While the above will truly give us a date that’s equivalent to the same time as right now but tomorrow, writing all of that code each time we want to perform such a date calculation can quickly get repetitive — so let’s move it into an extension on Date instead:

extension Date {
    func sameTimeNextDay(
        inDirection direction: Calendar.SearchDirection = .forward,
        using calendar: Calendar = .current
    ) -> Date {
        let components = calendar.dateComponents(
            [.hour, .minute, .second, .nanosecond],
            from: self
        )
        
        return calendar.nextDate(
            after: self,
            matching: components,
            matchingPolicy: .nextTime,
            direction: direction
        )!
    }
}

Calendar very often returns an optional Date from its various APIs, since the DateComponents used might contain an invalid combination of units (for example specifying the 31st of February as a date). However, in the above example we’re sure that our input is valid, so we force unwrap the result.

Above we also add support for specifying a SearchDirection, which enables us to move either forward or backward in time when calculating the next date — which in turn enables us to create two convenience APIs, one for accessing the current time tomorrow, and one for doing the same thing but for yesterday instead:

extension Date {
    static var currentTimeTomorrow: Date {
        return Date().sameTimeNextDay()
    }
    
    static var currentTimeYesterday: Date {
        return Date().sameTimeNextDay(inDirection: .backward)
    }
}

With the above in place, we can now simply pass .currentTimeTomorrow or .currentTimeYesterday to any API that accepts a Date, which is actually simpler than manually computing a date by adding a time interval — while also being much more robust as well.

From time intervals to date intervals

Apart from moving a date forwards or backwards in time, Calendar also contains a suite of APIs for dealing with dates in terms of intervals. While a Date value represents a single point in time, our perception of time often revolves more around intervals like days, weeks, and years — rather than absolute timestamps.

For example, let’s say that we’re working on an app that refreshes the content shown to each user at midnight within the user’s current timezone. So rather than constantly trying to fetch new content from our server, we’ll cache any loaded content until midnight that day — to only perform a new request once the next day has begun.

Rather than having to manually calculate the exact Date that corresponds to the start of the next day, let’s call the startOfDay method on our Calendar, in combination with our newly added currentTimeTomorrow convenience API — like this:

func contentDidLoad(_ content: Content) {
    let refreshDate = calendar.startOfDay(for: .currentTimeTomorrow)
    cache(content, until: refreshDate)
}

However, while the above will give us the exact date at which our new content should be ready, all clocks aren’t perfectly synchronized — meaning that if we’d perform our network request at that exact date, we might still end up getting yesterday’s content back as a response, in case our server’s clock is slightly behind our client-side one.

To fix that problem, let’s again enlist the help of TimeInterval, and simply add 100 seconds to our refresh date in order to give us a “buffer” that should be enough to account for any differences between our client and server-side clocks:

func contentDidLoad(_ content: Content) {
    let refreshDate = calendar.startOfDay(for: .currentTimeTomorrow)
    cache(content, until: refreshDate.addingTimeInterval(100))
}

The above is a great use case for a TimeInterval-based date manipulation, since we’re looking to shift our date in terms of absolute seconds, rather than retrieving a human-readable one.

Besides computing the start of a given interval, such as a day, Calendar also enables us to search for complete intervals as well — such as the next weekend that’ll come after a given date:

let nextWeekend = calendar.nextWeekend(startingAfter: Date())!

showPartySchedulingView(
    withStartDate: nextWeekend.start,
    endDate: nextWeekend.end
)

Finally, we can also use Calendar to retrieve an interval that a given date is a part of. For example, here’s how we could retrieve the start and end dates of the current day, month, and year:

let date = Date()
let today = calendar.dateInterval(of: .day, for: date)
let currentMonth = calendar.dateInterval(of: .month, for: date)
let currentYear = calendar.dateInterval(of: .year, for: date)

Along the same lines, here’s how we could compute the date interval for the following year, by adding one year to the current date, and then using that date to retrieve our interval:

let components = DateComponents(year: 1)
let todayNextYear = calendar.date(byAdding: components, to: Date())!
let nextYear = calendar.dateInterval(of: .year, for: todayNextYear)

Above we use DateComponents to manipulate the current date, which we also used in the initial example to specify the granularity of a date search. Just like how URLComponents can be used to define the individual components that make up a URL, a DateComponents value can be used to refer to various units of time — such as hours, days, and years — when either searching for a date, or when mutating one.

Conclusion

The differences in how time is represented within various cultures around the world (and throughout history), as well as how each of our calendar and time measurement systems include correcting mechanisms — such as leap days and daylight savings time — makes computing dates a lot more complex than what it first might seem like.

However, most of those complexities can be handled for us by the system, as long as we use the right APIs to perform our date calculations — and while this article didn’t cover all such APIs — I hope that it can act as a reference for when your code next needs to travel either forwards or backwards in time.

If you have any questions, comments, or feedback — then feel free to contact me either via Twitter or email.

Thanks for reading! 🚀