24 October 2024 6min read

AnchoredRelativeFormatStyle: The Hidden Replacement for Date RelativeFormatStyle

Today I made an exciting discovery: the existence of AnchoredRelativeFormatStyle. An API that, as of the time of writing this, barely appears in search results other than Apple’s sparse documentation. And to my surprise, it’s exactly what I’ve been looking for over the past few years.

Some time ago, I wrote about An exploration of FormatStyle as a Dependency. Thanks to my team’s work, we pushed that exploration further and developed an interesting solution (which I should write about!).

During that work, we defined a few date formatters for our application. One of the most interesting ones was a formatter for relative dates.

In the old times, with Foundation Formatter APIs, to get a formatted string for the relative time between two dates, things like “2 days ago” or “Tomorrow”, one could use:

localizedString(for date: Date, relativeTo referenceDate: Date) -> String 

In the new Foundation FormatStyle APIs, the way to do that is using the nice fluent API on a date:

.formatted(.relative(presentation: .named))

The new API looks elegant for quick code and tutorials. However, it becomes problematic when:

  • Writing tests
  • Creating development previews
  • Building features that need relative times between specific dates

All these issues arise from the same root problem: the implicit dependency on the current Date(). In the Formatter API you could pass the two dates but in the new Style API you just call formatted on a single Date, without a way to pass a second one.

In our system using swift-dependencies, we had workarounds with limitations. While investigating improvements, I stumbled upon AnchoredRelativeFormatStyle - which solves our exact problem by allowing two dates!

Where AnchoredRelativeFormatStyle comes from?

As someone who watches nearly every WWDC video and stays current with the Swift ecosystem, I was surprised I’d never heard of this API. A Google search returned just a couple of results with no substantial information. Was I imagining things?

Thankfully, we live in a world in which Foundation is open-source and easily accessible, so I could just go to GitHub and actually check not only the source code for AnchoredRelativeFormatStyle but also its history. Thanks to that, I could find the Foundation proposal that described the background of this new API: The DiscreteFormatStyle Protocol.

The proposal document is very interesting, but it took me a couple of reads to fully grasp it. I recommend you take a read, but its purpose is to improve the existing FormatStyle infrastructure to enhance its performance when used directly in user interfaces, read: SwiftUI.

If you’re the one implementing SwiftUI Text and accept a string made from a formatter like the ones mentioned above, you can see how the existing API limits your optimizations. Your system needs to keep the UI up to date with an Input, “the relative target date” that doesn’t change, but internally the output (the string) depends on a moving target, “now”, which you don’t have visibility on because remember, the API is tomorrow.formatted(.relative). This means you don’t have any alternative other than recomputing that formatted string at a high framerate, even if the output of the format will only change once a second or even once a minute.

This is why the proposal introduced DiscreteFormatStyle, a new protocol that allows FormatStyles to expose when an output is expected to be different given the style configuration. This allows UI implementors to optimize the refresh rate by asking the style when it needs a refresh to be scheduled.

Some existing formatters were able to conform to the protocol without problems, but RelativeFormatStyle couldn’t because its current design has Date() as a hidden dependency, which means the format implementation is not a pure function and thus breaks the assumptions of the infrastructure around the styles. From the infrastructure’s point of view, “tomorrow” is constant so it could optimize things, but the fact that every call to format with the same input can give a different output breaks the assumptions. This is a bit tricky to understand until you internalize how format infrastructure works, with the styles themselves being just value types that are suposed to just be a wrapper around a pure function.

DiscreteFormatStyle, a weird style

To solve the above, DiscreteFormatStyle’s design is a bit backwards from what we would expect. A naïve design for this API would be to have a parameter for the “now” in the style itself, something like:

tomorrow.formatted(.relative(to: now))

That would solve the problem I have with the existing relative style by removing the implicit dependency on Date(). But it wouldn’t solve the problem about performance as, even if now visible to us, the dependency on now that would change all the time wouldn’t be visible to the formatting infrastructure (aka format wouldn’t be a pure function).

So DiscreteFormatStyle works the other way around. The “anchor”, the data that doesn’t move, is part of the style, and the moving date is instead the instance where format is called on. Something like:

now.formatted(.relative(to: tomorrow))

But, as stated in the proposal, since this would break expectations from developers, the Foundation team decided to not offer the static method API at all. If you want to use this, you need to construct the FormatStyle yourself, which by the way, as we explored in my previous post, you can do for any FormatStyle, you just need to know the type.

Date.AnchoredRelativeFormatStyle(anchor: tomorrow).format(now)

We explicitly decided against adding such function for Date.AnchoredRelativeFormatStyle as that style always formats its anchor date, and only uses the actual format input as the reference date. Thus any spelling that starts with Date.now.formatted is ultimately misleading or very verbose. - A static factory function for Date.AnchoredRelativeFormatStyle.

Don’t tell them

I understand that reasoning, but it also makes this API basically hidden and unknown, which is quite unfortunate. I guess if the main use case is as part of the SwiftUI Text it makes more sense, but for me this API has way more uses than that.

Since we already have our own bit of sugar on top of FormatStyles that mixes it with swift-dependencies, it’s quite easy for me to improve this API. One could argue if I should do it, but I think for my use cases it’s totally fine.

If you want to also be naughty, it’s quite easy:

public struct NaughtyRelativeFormatStyle: FormatStyle {
    public typealias FormatInput = Date
    public typealias FormatOutput = String
    
    var referenceDate: Date
    
    public func format(_ value: Date) -> String {
        referenceDate.formatted(Date.AnchoredRelativeFormatStyle.init(anchor: value))
    }
}

extension FormatStyle where Self == NaughtyRelativeFormatStyle {
    public static func relative(
        to referenceDate: Date
    ) -> Self {
        NaughtyRelativeFormatStyle.init(
            referenceDate: referenceDate
        )
    }
}

tomorrow.formatted(.relative(to: now))

Of course the code above doesn’t implement all configuration options available on Foundation, but adding them is quite easy. I’ll leave that up to you so you make conscious decisions if you want to be naughty ;)

Conclusion

This was a very surprising day, I was not expecting to find such a hidden API that actually solved my problems. I still needed to do some work because we still want to use swift-dependencies, but at least I could use a modern API on new OSs and stop relying on the old Formatters. It also made me look again deeply into FormatStyles, which is a very nice API on the usage side but has some fascinating intricacies in its implementation.

If you enjoyed this post

Continue reading