12

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 ?

JAHelia
  • 6,934
  • 17
  • 74
  • 134
  • I am wondering how your codes build in xCode or compiles! In same time you are returning `() -> View` or `(View) -> View`! How could be not an issue?! – ios coder Aug 24 '21 at 02:41
  • copy-paste it as is (but remove the `.task` modifier) and it compiles. – JAHelia Aug 24 '21 at 05:44
  • @JAHelia: see this thread for a possible solution: https://developer.apple.com/forums/thread/652827. – koen Sep 25 '21 at 18:57
  • Maybe you can consider [creating an extension for OS checks](https://www.avanderlee.com/swiftui/conditional-view-modifier/#creating-a-bool-extension-for-os-specific-checks). – lochiwei Jan 17 '22 at 12:01

5 Answers5

26

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(@ViewBuilder _ transform: (Self) -> (some View)?) -> 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)
        }
    }
Samps
  • 799
  • 6
  • 12
7

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

Ralf Ebert
  • 3,556
  • 3
  • 29
  • 43
  • While this seems like it should work, it still crashes for me (eg. when trying to conditionally iOS 16 APIs on iOS 15) just like the version without .backport :(. https://stackoverflow.com/q/73278501/38729 – Tomáš Kafka Aug 08 '22 at 13:37
1

You can create a simple extension on View with @ViewBuilder

fileprivate extension View {
        @ViewBuilder
        var tabBarTintColor: some View {
            if #available(iOS 16, *) {
                self.tint(.red)
            } else {
                self.accentColor(.red)
            }
        }
    }

To use it just have it chained with your existing view

TabView() 
.tabBarTintColor
Ali
  • 2,427
  • 22
  • 25
-1

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.

malhal
  • 26,330
  • 7
  • 115
  • 133
  • 1
    the point is: I want to leverage iOS 15's modifiers in my iOS 14 project, such as `.listRowSeparator` and tens of other modifiers, so that when time comes to ditch iOS 14 support that will be a breeze. Currently I want to support both OS'es + full power of 15 – JAHelia Aug 24 '21 at 11:25
  • 1
    Changing style is different than `task` vs `onAppear`. You can run into this easily with target differences such as showing a list in a watch vs iOS. I use a `BridgeView` for this. It keeps all the ugly in the Bridge and the rest is reusable across targets – lorem ipsum Aug 24 '21 at 12:51
  • @loremipsum the OP wants this done in the same iOS target. – malhal Aug 25 '21 at 17:45
  • @JAHelia just target iOS 15. The tiny number of users that stay on iOS 14 can still download an old version of the app. – malhal Aug 25 '21 at 17:46
  • @malhal I just mentioned it as a similar use case where the same action is required when using a `.listRowSeparator` and differentiating between iOS 14 and iOS 15 and picking `.listStyle(CarouselListStyle())` for watch and `.listStyle(PlainListStyle())` for a phone. It can be handled in almost the exact same way. I had an answer posted that demonstrated this but deleted it because it is an alternative for the user not a solution for the problem. `.task` and `onAppear` are too different to be handled in the same way. – lorem ipsum Aug 25 '21 at 19:12
  • @loremipsum it's a little simpler than your answer. Simply do #if os(watchOS) .listStyle(CarouselListStyle()) #else .listStyle(PlainListStyle()) #endif – malhal Aug 26 '21 at 20:59
  • @malhal this is called `#if for postfix member expressions` which's only available in iOS 15 and Swift 5.5 – JAHelia Aug 29 '21 at 08:01
-5

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

}
ios coder
  • 1
  • 4
  • 31
  • 91
  • 3
    and if your view is 200 lines of code, you will end up maintaining ~400 lines of code – JAHelia Aug 25 '21 at 05:40
  • There is not such a big difference between iOS 15 and 14, mostly you would copy and paste the code and small edit on codes for what you want. – ios coder Aug 25 '21 at 08:33
  • Is it really a performance issue? – Thomas Aug 09 '22 at 15:29
  • Yes, if you use the other answer you make SwiftUI to check every single time about availability of a version! In my answer SwiftUI would check it just for one time! The other person just complained about having codes for same thing 2 times, but he is sacrificing the performance instead having some few codes more, which having more few codes would not effect the performance. Because in each codes SwiftUI would not check for availability of a version and it would just read the codes and run it. – ios coder Aug 09 '22 at 20:38