iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
👣

Handling OS version checks effectively in SwiftUI

に公開2

As OS updates occur, the number of available modifiers continues to grow.
While it becomes more convenient, if you wanted to add a "Liquid Glass" modifier (recently introduced) to an existing app, for example, I feel like it would end up looking like this:

import SwiftUI

if #available(iOS 26.0, *) {
  Button {
    // do some action
  } label: {
    Text("Do some action")
  }
  .glassEffect()
} else {
  Button {
    // do some action
  } label: {
    Text("Do some action")
  }
}

I have a feeling it would end up looking like that (right? right??).

However, this approach results in two Views being created in various places, making it redundant and difficult to read. Therefore, I think the following technique is commonly used:

import SwiftUI

extension View {
  @ViewBuilder
  func glassEffectIfOver26() -> some View {
    if #available(iOS 26.0, *) {
      self
        .glassEffect()
    } else {
      self
    }
  }
}

Button {
  // do some action
} label: {
  Text("Do some action")
}
.glassEffectIfOver26()

While this seems clean, there is a risk that similar extensions will multiply endlessly.
Fundamentally, what we ideally want is to write it like this:

Button {
  // do some action
} label: {
  Text("Do some action")
}
if #available(iOS 26.0, *) {
  .glassEffect()
}

As of now (late December 2025), due to SwiftUI's syntax, you cannot directly write if #available in the middle of a View modifier chain.
Therefore, I came up with the following extension.


extension View {
  /// Conditionally applies modifiers based on the current OS version.
  ///
  /// SwiftUI does not allow writing `#available` checks directly inside a
  /// modifier chain. `osCondition(_:)` acts as an escape hatch that enables
  /// OS-specific branching while keeping the modifier-style syntax.
  ///
  /// This is especially useful when adopting new system-provided modifiers
  /// (such as those introduced in newer iOS releases) without duplicating views
  /// or creating many one-off extension methods.
  ///
  /// Example:
  /// ```swift
  /// Text("Hello")
  ///   .osCondition {
  ///     if #available(iOS 26.0, *) {
  ///       $0.glassEffect()
  ///     } else {
  ///       $0
  ///     }
  ///   }
  /// ```
  ///
  /// - Parameter modifier: A closure that receives the current view and returns
  ///   a modified view. The closure is evaluated unconditionally, so OS version
  ///   checks should be performed inside using `#available`.
  @ViewBuilder public func osCondition<Content: View>(
    @ViewBuilder modifier: (Self) -> Content
  ) -> some View {
    modifier(self)
  }
}

This is a simple extension that merely executes the closure passed as an argument.
However, at the call site, it can be written as follows:

Button {
  // do some action
} label: {
  Text("Do some action")
}
.osCondition { view in
  if #available(iOS 26.0, *) {
    view
      .glassEffect()
  } else {
    view
  }
}

Wait, isn't this good enough?
With that in mind, I looked around and found the following library:

https://github.com/Aeastr/Conditionals

I think the concept is similar.
While using a library is a good option if you want a more feature-rich and systematic solution, personally, I feel that this approach is well-balanced for when you just want to perform a bit of OS-specific branching.
If anyone knows of a better way, please let me know!

Discussion