SwiftUI: using view modifiers between different iOS versions without #available

Viewed 1941

I use the following code snippet (in Xcode 13 Beta 5 and deployment target set to 14.0) to apply view modifiers conditionally according to iOS version:

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .modifyFor(iOS14: {
                $0.onAppear {
                    //do some stuff
                }
            }, iOS15: {
                $0.task { //<---- Error: 'task(priority:_:)' is only available in iOS 15.0 or newer
                    //do some stuff
                }
            })
    }
}

struct CompatibleView<Input: View,
                      Output14: View,
                      Output15: View>: View {
    var content: Input
    var iOS14modifier: ((Input) -> Output14)?
    var iOS15modifier: ((Input) -> Output15)?
    
   @ViewBuilder var body: some View {
        if #available(iOS 15, *) {
            if let modifier = iOS15modifier {
                 modifier(content)
            }
            else { content }
        }
        else {
            if let modifier = iOS14modifier {
                 modifier(content)
            }
            else { content }
        }
    }
}

extension View {
    func modifyFor<T: View, U: View>(iOS14: ((Self) -> T)? = nil,
                                     iOS15: ((Self) -> U)? = nil) -> some View {
         CompatibleView(content: self,
                                  iOS14modifier: iOS14,
                                  iOS15modifier: iOS15)
    }
}

this code works great as long as I don't use iOS 15's view modifiers, but if I want to use any of those modifiers (like Task for ex.) then I need to use the #available directive which's an option I don't wanna opt in, because my codebase is large, there are many parts that should adopt the new iOS 15 modifiers and by using #available everywhere in the code will make it looks like a dish of Lasagna.

how to make this piece of code compiles in a clean way and without using the #available check ?

4 Answers

The best solution for so far I've figured out is to add simple modify extension function for view and use that. It's useful if availability check for modifier is needed only in one place. If needed in more than one place, then create new modifier function.

public extension View {
    func modify<Content>(@ViewBuilder _ transform: (Self) -> Content) -> Content {
        transform(self)
    }
}

And using it would be:

Text("Good")
    .modify {
        if #available(iOS 15.0, *) {
            $0.badge(2)
        } else {
            // Fallback on earlier versions
        }
    }

EDIT:

@ViewBuilder
func modify<Content: View>(@ViewBuilder _ transform: (Self) -> Content?) -> some View {
    if let view = transform(self), !(view is EmptyView) {
        view
    } else {
        self
    }
}

This allows us not to define fallback if not required and the view will stay untouchable.

Text("Good")
    .modify {
        if #available(iOS 15.0, *) {
            $0.badge(2)
        }
    }

There is no way to do this without 'if #available', but there is a way to structure it in a somewhat clean way.

Define your own View Modifier on 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
        }
    }
}

You can then use these like this:

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

Blog post about it: Using iOS-15-only View modifiers in older iOS versions

There is no point because even if you did back-port a modifier named task (which is how this problem is normally solved) you won’t be able to use all the magic of async/await inside which is what it was designed for. If you have a good reason for not targeting iOS 15 (I don’t know any good ones) then just continue to use onAppear as normal and either standard dispatch queue async or Combine in an @StateObject.

There is no logical use case for that modifier for the issue you are trying to solve! You have no idea, how many times your app would check your condition about availability of iOS15 in each render! Maybe 1000 of times! Insane number of control which is totally bad idea! instead use deferent Views for each scenarios like this, it would checked just one time:

WindowGroup {
    
    if #available(iOS 15, *) {
        
        ContentView_For_iOS15()
        
    }
    else {
        
        ContentView_For_No_iOS15()
        
    }

}
Related