Handling different iOS versions in a View body
When using a SwiftUI modifier that was added in a new iOS version, you'll get an error in projects that are configured to be compatible with older iOS versions:
... is only available in iOS 26.0 or newer
This article shows how to check for the availability of an iOS version in a SwiftUI View body to make use of new View modifiers that have been added in recent iOS releases.
Checking for iOS version / API Availability
In regular Swift code, you can use this to check for an iOS version at runtime:
if #available(iOS 26, *) {
// ...
}
if #unavailable(iOS 26) {
// ...
}
WARNING
if #available(iOS 26.0, *) { ... } will fail to build with a non-beta Xcode while iOS 26 is in beta / until the final Xcode version for iOS 26 is released. Make sure to keep those changes in a separate branch if you need to do maintenance releases in parallel.
↗ Swift: Checking API Availability
if #available in modifier chains
You cannot use such an if #available-statement directly in the middle of a chain of modifiers. As a solution, you can use this View extension:
// » SwiftUI Garden
// » https://swiftui-garden.com/Articles/Handling-different-iOS-versions-in-a-View-body
import SwiftUI
public extension View {
func modify(@ViewBuilder transform: (Self) -> some View) -> some View {
transform(self)
}
}
Then use it like this:
List {
Section {
}
.modify { content in
if #available(iOS 26, *) {
content
.sectionIndexLabel(Text("..."))
} else {
content
}
}
}
WARNING
- Do NOT forget to return content in the else block or the View will be missing in the old iOS version
- Do NOT use this for other conditions that can change during runtime (static things like iOS version, "iPhone or iPad" are fine though). Otherwise the View hierarchy will change and state will be thrown away / transitions will break.
Tweaking values
If you only want to tweak values like paddings between different iOS versions, a generic function like this is handy:
public func value<T>(_ ios26: T, ios18: T) -> T {
if #available(iOS 26, *) {
ios26
} else {
ios18
}
}
Then you can go:
.padding(.trailing, value(0, ios18: 16))
Custom View extensions, .backport
If you need the same modifier in many places, a reusable view extension comes in handy:
extension View {
@ViewBuilder func compatibleSectionIndexLabel(_ label: Text) -> some View {
if #available(iOS 26, *) {
self.sectionIndexLabel(label)
} else {
self
}
}
}
Here is a helpful idea about that from Dave DeLong: Don't add the modifier to the View itself but to a wrapper View called backport:
↗ Dave DeLong: Simplifying Backwards Compatibility in Swift
// » SwiftUI Garden
// » https://swiftui-garden.com/Articles/Handling-different-iOS-versions-in-a-View-body
import SwiftUI
struct Backport<Content> {
let content: Content
}
extension View {
var backport: Backport<Self> { Backport(content: self) }
}
extension Backport where Content: View {
@ViewBuilder func sectionIndexLabel(_ label: Text) -> some View {
if #available(iOS 26, *) {
content.sectionIndexLabel(label)
} else {
content
}
}
}
Then use it like this:
List {
Section {
}
.backport.sectionIndexLabel(Text("..."))
}
This has the advantage that you can use the same name for the modifier and can easily find all those places where such a modifier was used. This will be handy if later the old version is not to be supported anymore and the code can be removed.
Common implementations of such modifiers can be found here as package (thanks @jordanmorgan)