Checking for availability of iOS 15 in a SwiftUI view body

by @ralfebert · updated November 02, 2021
Xcode 13 & iOS 15
Advanced iOS Developers
English

When you use a SwiftUI modifier that was added in iOS 15, you'll get an error in projects that are configured to be compatible with older iOS versions:

... is only available in iOS 15.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.

Example

For example, there is a new modifier ↗ .badge() in iOS 15 that allows to set a badge to a tabItem:

TabView {
    Color.yellow
        .tabItem {
            Label("Example", systemImage: "hand.raised")
        }
        .badge(5)
}

Checking for API Availability

Theoretically, you could use this runtime check in projects with deployment target iOS 14:

if #available(iOS 15, *) {
    // ...
}

↗ Swift: Checking API Availability

Unfortunately, you cannot use an if-statement in the middle of a chain of modifiers.

Now, wasn't there something new in Swift 5.5 about this? Yes and no. You can now use #if in the middle of a modifier chain. But that's a compile time check, so you can use it to conditionally use a platform-specific modifier in a cross-platform app:

TextField("Field", text: $name)
#if os(OSX)
    .prefersDefaultFocus(in: namespace)
#endif

↗ #if Conditional Compilation Block
↗ #if for postfix member expressions

Checking for API Availability with a custom View modifier

One way to implement such a check is to create your own View Modifier that is compatible with the old iOS version. Here is an example for a .withBadge()` modifier that can be used on iOS 14 (where it does nothing) and iOS 15 (where it shows the badge):

struct WithBadgeModifier: ViewModifier {
    var count: Int

    func body(content: Content) -> some View {
        if #available(iOS 15.0, *) {
            content.badge(count)
        } else {
            content
        }
    }
}

extension View {
    func withBadge(count: Int) -> some View {
        modifier(WithBadgeModifier(count: count))
    }
}

↗ Building custom View modifiers in SwiftUI

Improved structure

Here is a helpful idea from Dave DeLong: Don't add the modifier to the View itself but to a wrapper View:

struct Backport<Content> {
    let content: Content
}

extension View {
    var backport: Backport<Self> { Backport(content: self) }
}

extension Backport where Content: View {
    @ViewBuilder func badge(_ count: Int) -> some View {
        if #available(iOS 15, *) {
            content.badge(count)
        } else {
            content
        }
    }
}

This modifier can than be used like this:

TabView {
    Color.yellow
        .tabItem {
            Label("Example", systemImage: "hand.raised")
        }
        .backport.badge(5)
}

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.

See also

↗ Dave DeLong: Simplifying Backwards Compatibility in Swift
↗ How to use iOS15-specific modifiers in SwiftUI on iOS 14 and earlier?
↗ SwiftUI: using view modifiers between different iOS versions