In the world of web frontend development, ‘progressive enhancement’ refers to the art of supporting as many different browser engines and configurations with the same codebase as possible.
It is far less of a challenge on the web today than during the heyday of the browser wars. In 2026, Chrome’s Blink reigns supreme, while Gecko (Firefox) and WebKit (Safari) occupy smaller niches and largely implement the same core browser features. Still, the design philosophy of progressive enhancement remains central to how web frontends are built. As modern browsers develop new capabilities, like WebGPU and Temporal, JavaScript developers continue to rely on feature detection to gracefully handle scenarios where some or other functionality is unavailable on the user’s machine.
Thanks to the dynamic nature of JavaScript, this typically requires no special syntax and resembles how we iOS developers once used respondsToSelector in Objective-C:
if ("geolocation" in navigator) {
// Access navigator.geolocation APIs
} else {
// Do something else
}
Because Apple’s platforms evolved in a different, far more controlled way than the world wild web, the modern Swift equivalent looks nothing like this, however.
Unlike the fluid, version-less environment of the browser, iOS and macOS apps run on distinct OS releases - iOS 18.6.2, macOS 26.3, and so on - each with a finite, known set of APIs. When you define a minimum deployment target in Xcode, you opt into using features available strictly on that and future OS versions - something that’s unambiguous at compile time.
Given this, the design decision by the Swift language team was to make feature detection expressible by marking whole types and conditional branches as version-specific:
@available(iOS 13.0, *)
final class CustomCompositionalLayout: UICollectionViewCompositionalLayout { … }
func createLayout() -> UICollectionViewLayout {
if #available(iOS 13, *) {
return CustomCompositionalLayout()
} else {
return UICollectionViewFlowLayout()
}
}
This natural-language-like syntax stands out against regular Swift code in a way that’s reminiscent of #if DEBUG or #if os(macOS) compiler directives, except that it’s tightly integrated into control flow. So tightly in fact that it starts to break down, ironically, in the fastest-evolving set of Apple APIs where you might want it most: SwiftUI.
The problem with SwiftUI#
SwiftUI relies heavily on method chaining and opaque return types (some View). This makes it difficult to incorporate availability checks without forcing larger structural changes to your UI code.
Consider this SwiftUI button that uses modern Liquid Glass styling:
Button {} label: {
Label("Hello, world!", systemImage: "globe")
}
.buttonStyle(.glassProminent) // Only available on AppleOS 26+
When you lower the deployment target to, say, macOS 15, this code no longer compiles, and Xcode will offer three potential fixes:
'glassProminent' is only available in macOS 26.0 or newer
* Add 'if #available' version check
* Add '@available' attribute to enclosing property
* Add '@available' attribute to enclosing struct
None of these are going to work the way you’d want.
The @available attribute will force you to define separate version-specific button types, similarly to the UICollectionViewLayout example above. You will end up with a pile of clunky abstractions around something as basic as a button - hardly a good result.
It’s more expressive, and somewhat in the spirit of SwiftUI, to branch directly in the context of the containing view. However, you still cannot escape unseemly code duplication this way, as each branch must contain a complete returnable structure.
if #available(macOS 26, *) {
Button {} label: {
Label("Hello, world!", systemImage: "globe")
}
.buttonBorderShape(.capsule)
.buttonStyle(.glassProminent)
} else {
Button {} label: {
Label("Hello, world!", systemImage: "globe")
}
.buttonBorderShape(.capsule)
}
What you really want is the power to do what is possible with compilation directives, i.e. wrap just the version-specific modifier and leave the rest of the chain shared. Unfortunately, that’s not permitted in Swift:
Button {} label: {
Label("Hello, world!", systemImage: "globe")
}
.buttonBorderShape(.capsule)
if #available(macOS 26, *) { // ❌ Syntax error
.buttonStyle(.glassProminent)
}
This is where progressive enhancement of SwiftUI interfaces becomes a real pain. Supporting Liquid Glass, or other future and current version-specific UI features, should not force you to gate whole view fragments with if #available just to get a single modifier to compile.
A web equivalent of this would be maintaining entirely separate UI components for Firefox and Chrome just because one of them lacks support for a new CSS feature. This is not how progressive enhancement is meant to work - so what might be a more elegant solution?
with {} a little help from my friends#
Something I’ve used in my projects and recommend to you is a riff on the conditional view modifier idea. In his post Antoine van der Lee explores a few different takes on a helper like this, but what I personally prefer is this minimalist implementation:
// View+with.swift
import SwiftUI
extension View {
@ViewBuilder
func with<V: View>(
@ViewBuilder _ transform: (Self) -> V
) -> some View {
transform(self)
}
}
At a glance, this may look almost too simple to do anything useful, so let’s see this helper in action:
Button {} label: {
Label("Hello, world!", systemImage: "globe")
}
.buttonBorderShape(.capsule)
.with { view in
if #available(macOS 26, *) {
view.buttonStyle(.glassProminent)
} else {
view
}
}
Now you are able to ‘capture’ the state of the view at any point in the chain in a closure, where it can be conditionally modified - or passed on as is.
A major benefit of a generic helper like this is that it lets the if #available check draw attention to itself directly in the context where it’s used. The Liquid Glass compatibility logic is localized and obvious, and the same helper can remain unchanged as older OS versions are dropped and new ones are adopted.
If you’re working with toolbars and want similar functionality on the ToolbarItem, simply add one more extension:
// ToolbarItem+with.swift
import SwiftUI
extension ToolbarItem {
func with<C: ToolbarContent>(
@ToolbarContentBuilder _ transform: (Self) -> C
) -> C {
transform(self)
}
}
This lets you make Liquid Glass tweaks to your toolbar items and align your design across OS versions in the same way:
.toolbar {
ToolbarItem { … }
.with { item
if #available(macOS 26, *) {
item.sharedBackgroundVisibility(.hidden)
} else {
item
}
}
}
Aren’t ‘conditional modifiers’ an anti-pattern?#
There is a good reason why this functionality is not available by default in SwiftUI, and you should exercise caution if you decide to use it for anything other than progressive enhancement, like in the examples above.
If you replace the if #available check inside the with closure with dynamic conditions, such as branching on a property value, you interfere with SwiftUI’s ability to reason about view identity. This will break animations and introduce subtle state bugs: Chris Eidhof covers these pitfalls in detail in his objc.io post.
Luckily, OS availability is static for the lifetime of the app, so when used exclusively for version checks, this pattern is safe from those problems.