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:

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:

Of course, there are a few downsides:

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.

Thank you for registering!
You’ll retrieve all of my latest news!

Your email is sadly invalid.
Can you try again?

An error occurred while registering.
Please try again