SwiftUI backward-compatibility with Disfavored Overload
Each year, new SwiftUI APIs are gladly arriving for our apps at WWDC.
And each year, those new APIs are central to make your app a first class citizen on the newer OS, but as you know, it’s painful to maintain backward compatibility with older OSes when using newly introduced modifiers.
Naive approach
Let’s say we’d like to use privacySensitive()
that was introduced with iOS 15 on an app that is still compatible with iOS 13 or 14.
Sadly, the most straightforward approach of just “wrapping with availability” for our modifier won’t compile as of today:
struct MyView: View {
var body: some View {
Text("My super view!")
if #available(iOS 15, *) {
// This will not compile!
.privacySensitive()
}
}
}
To solve this, we can extract the content of the view away, to prevent repeating ourself:
struct MyView: View {
var body: some View {
if #available(iOS 15, *) {
content
.privacySensitive()
} else {
// Custom fallback implementation?
content
}
}
var content: some View {
Text("My super view!")
}
}
But even with this approach, it’s still painful to use:
- we’ll have to repeat the same trick for all of our views that requires it
- if you have another modifier for iOS 16+ only, and another for iOS 17+ only, it became a nightmare not repeating yourself!
Custom modifier
Extracting the availability in a custom modifier is the next approach. It’s reusable and prevent boilerplate code.
That is said, in our example, a simple extension is enough; it’s not really a modifier, but it’s not actually required since we do not need store any value in the modifier struct.
extension View {
@ViewBuilder
public func privacySensitiveWhenAvailable() -> some View {
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) {
// We call the original API here
privacySensitive()
} else {
// Optionally, we can implement a fallback API
self
}
}
}
We can then use it directly in our views:
struct MyView: View {
var body: some View {
Text("My super view!")
.privacySensitiveWhenAvailable()
}
}
It’s nicer, but we can do better
@_disfavoredOverload in action
This one is a trick @Jegnux gave me a couple years back.
@_disfavoredOverload
is not really a documented feature. It’s even discouraged to use them outside of swift repo as stated in the only documentation I found for it.
The fun thing is that this attribute was introduced to workaround a bug, but it’ll allow us to name our modifier exactly like the official one without risking an infinite recursion loop.
Demo:
extension View {
@ViewBuilder
@_disfavoredOverload // The magic happens here
public func privacySensitive() -> some View {
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) {
// Thanks to disfavoredOverload attribute,
// it’s not ours, but SwiftUI’s one that gets called here.
privacySensitive()
} else {
// Optionally, we can implement a fallback API
self
}
}
}
And now, anywhere in your app, you can use the API just like if it was the official, it’ll just compile.
struct MyView: View {
var body: some View {
Text("My super view!")
.privacySensitive()
}
}
Main benefits are:
- Readability : It’s just like Apple’s code, right?
- Discoverability : just use the API you know, and the magic can be hidden from the junior devs
- Code stripping : the day you drop the incompatible OSes, you’ll be calling the SwiftUI API direclty and your extension will become dead code that will be stripped. But you’ll have removed it alongside your update right?
Of course, there are a few downsides:
- Hiding non-compatibility : for API with no fallback, behavior won’t occur on older OSes, and you might forget about this! It’s especially dangerous when you repose part of your feature on the modifier behavior to actually make something. This one can be mitigated by simply providing a fallback implementation for older OSes.
- Reliability : This rely on an Underscored Attributes, that is strongly discouraged to use outside of Swift monorepo itself. But the cool thing is that the day it stops working, it’ll not stop working live on a device, since this attribute is resolved at build time. If something goes wrong, it’s likely that you’ll catch it up while building or testing your app.
My take on this
I do think that sometime a custom method or modifier is better, because you can explicit that a code hides an incompatibility, or that the fallback behavior is not implemented.
But in cases were you have your own implementation for the fallback, hiding the code with disfavoredOverload becomes handy!
I hope this small Swift compiler trick will be as helpful for you it’s been for me in your SwiftUI apps!
Don’t miss a thing!
Don't miss any of my indie dev stories, app updates, or upcoming creations!
Stay in the loop and be the first to experience my apps, betas and stories of my indie journey.