Conditional availability based on OS version

How do I specify a conditional availability of a type or an extension, etc, so that either my version or the OS version is getting used depending upon OS version? Example pseudo code (doesn't compile):

@available (iOS, unavailable: 15)
enum HorizontalEdge {
	case leading, trailing
}

@available (iOS, unavailable: 15)
extension View {
	func swipeActions<T>(edge: HorizontalEdge = .trailing, allowsFullSwipe: Bool = true, @ViewBuilder content: () -> T) -> some View where T : View {
		self // do nothing until iOS 15
	}
}

I can think of a more cumbersome solution that uses a different name for "swipeActions", say "swipeActions2" and either calls through OS "swipeActions" on iOS15+ or does nothing on elder systems. Although if there is a way to solve this keeping the name as is, I'd like to do that to keep the main app code as undisturbed as possible (that's why i don't want pollute the main app code base with #if's availability blocks scattered here and there).

1 Like

I don't think there's a way to do what you want with conditional availability. This is the best approach I've seen so far: Simplifying Backwards Compatibility in Swift | Dave DeLong

Here's a playground for "backporting" .swipActions based on the approach described in that article:

import PlaygroundSupport
import SwiftUI

struct Backport<Content> {
    let content: Content

    init(_ content: Content) {
        self.content = content
    }
}

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

extension Backport {
    enum HorizontalEdge {
        case leading
        case trailing
        
        @available(iOS 15, *)
        var shadowed: SwiftUI.HorizontalEdge {
            switch self {
            case .leading: return .leading
            case .trailing: return .trailing
            }
        }
    }
}

extension Backport where Content: View {
    @ViewBuilder
    func swipeActions<T: View>(edge: HorizontalEdge = .trailing, allowsFullSwipe: Bool = true, content: () -> T) -> some View {
        if #available(iOS 15, *) {
            self.content.swipeActions(edge: edge.shadowed, allowsFullSwipe: allowsFullSwipe, content: content)
        } else {
            // Do something else if you want to...
            self.content
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            List {
                Text("Swipe Me")
                    .backport.swipeActions(edge: .trailing, allowsFullSwipe: true) {
                        Button(role: .destructive, action: {}) {
                            Image(systemName: "trash.fill")
                        }
                    }
            }
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())
2 Likes

Nice. Something is not entirely right here though as I can't specify two buttons inside one "swipeActions" block (although I can workaround it by placing them into two separate swipeActions blocks). figuring out..

// ..... as above
.backport.swipeActions(edge: .trailing, allowsFullSwipe: true) {
    Button(role: .destructive, action: {}) {
        Image(systemName: "trash.fill")
    }
    Button(action: {}) {
        Image(systemName: "photo")
    }
}

Error: Type '()' cannot conform to 'View'

I forgot to mark the content closure as a ViewBuilder. It should be:

extension Backport where Content: View {
    @ViewBuilder
    func swipeActions<T: View>(edge: HorizontalEdge = .trailing, allowsFullSwipe: Bool = true, @ViewBuilder content: () -> T) -> some View {
        if #available(iOS 15, *) {
            self.content.swipeActions(edge: edge.shadowed, allowsFullSwipe: allowsFullSwipe, content: content)
        } else {
            // Do something else if you want to...
            self.content
        }
    }
}
1 Like

Great, thank you.

I managed to sugarize it a bit further:

view
    .available(.ios15)
    .swipeActions(edge: .trailing, allowsFullSwipe: true) {
        ....
    }

This makes it similar to #available (iOS 15, *) checks. And then when time comes - just nuke all them strings - one big red commit.

Still, it would be even better if it was possible without changing the main app sources.