How to use iOS15-specific modifiers in SwiftUI on iOS 14 and earlier?

There are many new iOS15-specific modifiers that were added in SwiftUI. For example, we have a .focused() modifier, which can be used like this:

TextField("Username", text: $username)
    .focused($focusedField, equals: .username)

However, this code fails to compile if the app supports iOS 14 and earlier. How can I make this code to compile? Ideally, I'd like to do something like this:

TextField("Username", text: $username)
    #if os(iOS, 15.0, *)
    .focused($focusedField, equals: .username)
    #endif

But obviously this won't work because #if os() can only specify the target OS, not the version..

Thanks!

Answers

The keyword you're looking for is #available. Use an if-else block, not #if and #endif, putting the iOS 14 code in the else part of the block.

TextField("Username", text: $username)
    if #available(iOS 15.0, *) {
        .focused($focusedField, equals: .username)
    }
    else {
        // Put the iOS 14 equivalent code here.
    }

Unfortunately, you cannot use an "if" statement like that / in the middle of a chain of modifiers.

In Swift 5.5 #if can be used like that (see #if for postfix member expressions ) - but that is a compile time check so you cannot use this to check for an iOS version at runtime.

I think the only way is to build your own modifier that's compatible with the old iOS version. I had the same problem with the new badge() modifier, so here is an example:

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))
    }
}

Another way to structure is to build a .backport modifier so you can use the same name for the modifier (.backport.badge(10)) and can find all those places where a new modifier was used conditionally (thanks Dave deLong):

struct Backport<Content: View> {
    let content: Content
}

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

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