Hello,
I'm running into an issue with the if #available(iOS 15, *)
check. It's my understanding this is a run-time check which is what I require but I'm running into an issue on a SwiftUI utility I'm making where even though I wrap the caller to a function that uses an iOS 15 API the compiler is still trying to force me to guard the offending API at the implementation site.
To illustrate below I've included the simplest example of what I'm working on, the idea is to provide an if
SwiftUI View modifier to allow one of two transform closures to be called based on iOS support.
extension View {
@ViewBuilder func if_iOS15<Content>(_ trueTransform: (Self) -> Content,
else falseTransform: (Self) -> Content) -> some View where Content: View {
if #available(iOS 15, *) {
trueTransform(self)
} else {
falseTransform(self)
}
}
}
// Desired Usage
struct MyView: View {
var body: some View {
Circle()
.if_iOS15 { circle in
circle.fill(.cyan) // Compiler error: 'cyan' is only available in iOS 15.0 or newer
} else: { circle in
circle.fill(Color.blue)
}
}
}
My actual implementation is a bit more fleshed out and doesn't limit itself to a specific hardcoded iOS version regardless, the example above is the minimum to expose the issue.
You can see in the MyView
example, we're calling the if
modified on Circle
, the if
modifier method performs a run-time checking using #available
and calls the appropriate "transform" closure. This guarantees at run-time the code circle.fill(.cyan)
will only be called on devices running iOS 15.
Yet when I build the compiler is having none of that and is throwing the error "'cyan' is only available in iOS 15.0 or newer".
I would kind of expect the compiler to use static analysis and know that circle.fill(.cyan)
is in a closure that is already guarded by an #available
directive.
// Compiler forced workaround
struct MyView2: View {
var body: some View {
Circle()
.if_iOS15 { circle in
guard #available(iOS 15, *) else { return circle.eraseToAnyView() }
return circle.fill(.cyan).eraseToAnyView()
} else: { circle in
return circle.fill(Color.blue).eraseToAnyView()
}
}
}
extension View {
func eraseToAnyView() -> AnyView {
return AnyView(self)
}
}
The compiler forces me to add a guard to the offending code as shown in MyView2
above even though it's guaranteed to be safe.
This really complicates my example as now I've got three paths in my version checking logic:
- if iOS 15 then, if NOT iOS 15 return unmodified View (type erased)
- if iOS 15 then, if iOS 15 call iOS 15 API (cyan fill)
- if NOT iOS 15 then, call older version API (blue fill)
Circle() // -> Circle
.if_iOS15 { circle in
guard #available(iOS 15, *) else { return circle // -> Circle } // Error: Cannot convert return expression of type 'Circle' to return type 'some View'
return circle.fill(.cyan) // -> some View
} else: { circle in
return circle.fill(Color.blue) // -> some View
} -> ?
SwiftUI super complicates this due to @ViewBuilder
having to return some View
which means on the completely redundant but compiler enforced guard #available(iOS 15, *) else { return circle.eraseToAnyView() }
we have to erase Circle
to AnyView
and then erase the other instances so that they all appear as the same concrete type, this wouldn't be needed at all if the redundant guard wasn't there as the other two instances return some View
.
In what IMHO should be a perfectly working example this wouldn't be an issue:
Circle() // -> Circle
.if_iOS15 { circle in
circle.fill(.cyan) // -> some View
} else: { circle in
circle.fill(Color.blue) // -> some View
} // -> Circle
I kind of if feel the "is only available in iOS 15.0 or newer" error I'm getting is a false negative as I've already guarded against calling that API on iOS 15 it's just the compiler doesn't seem smart enough to agree with me.
It seems very unintuitive that a run-time statement like if #available(...)
is being forced on me in such a limiting way at compile time for no good reason that I can see.
Is this a known limitation, is there any way I can force the compiler to give me a break when I've complied already by adding a run-time check?