I'm trying to figure out an efficient way to conditionally apply modifiers within a swift view
In this example, I want to be able to turn off the overlay and tapGesture
struct TestView: View {
var showOverlay:Bool = true
var activeTap:Bool = true
var body: some View {
Text("Hello, World!")
.overlay(Circle().foregroundColor(.red))
.onTapGesture {
print("tapped")
}
}
}
both the .overlay and .onTapGesture are extensions on View that return someView, so I figure I should be able to write a new conditional modifier something like
extension View {
func ifTrue<T:View>(_ condition:Bool, apply:(T) -> T) -> some View {
if condition {
return apply(self) // 'Self' is not convertible to 'T'
}
else {
return self
}
}
}
so my view would become something like
struct TestView: View {
var showOverlay:Bool = true
var activeTap:Bool = true
var body: some View {
Text("Hello, World!")
.ifTrue(showOverlay){
$0.overlay(Circle().foregroundColor(.red))
}
.ifTrue(activeTap){
$0.onTapGesture { // Cannot invoke 'onTapGesture' with an argument list of type '(@escaping () ())'
print("tapped")
}
}
}
}
I'm hitting two problems
#1 the view extension won't compile
return apply(self) // 'Self' is not convertible to 'T'
and the tapGesture isn't happy
$0.onTapGesture { // Cannot invoke 'onTapGesture' with an argument list of type '(@escaping () ())'
is there a sensible generic way to achieve conditional modifiers?
if not - how should I approach this?
zero docs on what TupleView does as far as I can see.
re performance - AnyView (kinda) warns about the view hierarchy being destroyed when the type of view in the AnyView changes. I'm guessing that a re-build when the conditional changes is inexpensive providing it isn't being done all the time.
presumably the TupleView would have to rebuild when it switches from one side of the tuple to the other anyway.
that's pure guesswork though, and I'm not even sure how I would measure performance...
@ViewBuilder
var myView: some View {
if condition {
ViewA()
} else {
ViewB()
}
}
This will use buildEither method and wrap both view into a SwiftUI internal view meant for conditions like that.
Keep in mind that SwiftUI might still wrap it into AnyView. You can workaround the issue by creating a generic Group alike type. And PLEASE don‘t abuse Group for the purpose of what ViewBuilder should do, it‘s not meant for these tasks.
Often you can find a way to make a modifier have no effect, although the way is different for each modifier. In your example:
You can set the overlay opacity to 0 to disable it.
You can create a TapGesture and add it with the .gesture modifier instead of using .onTapGesture. The .gesture modifier takes a GestureMask that you can use to disable the gesture.
Playground example:
import SwiftUI
import PlaygroundSupport
struct RootView: View {
@State var counter: Int = 0
@State var counterTapIsEnabled = true
var body: some View {
VStack(spacing: 20) {
Text("Count: \(counter)")
.overlay(tapOverlay)
.gesture(tapGesture, including: tapGestureMask)
Button(buttonString) {
self.counterTapIsEnabled.toggle()
}
}
}
private var tapOverlay: some View {
return Circle()
.foregroundColor(Color.red.opacity(overlayOpacity))
}
private var overlayOpacity: Double {
return counterTapIsEnabled ? 0.2 : 0.0
}
private var tapGesture: some Gesture {
return TapGesture()
.onEnded { self.counter += 1 }
}
private var tapGestureMask: GestureMask {
return counterTapIsEnabled ? .all : .subviews
}
private var buttonString: String {
if counterTapIsEnabled {
return "Turn Taps Off"
} else {
return "Turn Taps On"
}
}
}
PlaygroundPage.current.setLiveView(RootView())
What @mayoff suggests is the best alternative when possible. View updates will be more efficient, and automatic animations more robust, if you apply modifiers with values that conditionally take no effect, instead of conditionally applying or not applying the entire modifier. If that's not possible, what @DevAndArtist suggests would be the way to go. You should be able to build new modifiers using either technique, in addition to self-contained views.
@mayoff - thanks for that. One of the things that sent me on this hunt was the inability to find a null gesture. I wasn't aware of the gesture modifier option.
Thanks particularly for putting that together as a playground
@Joe_Groff I'm still not following the ViewBuilder approach. Is that a way to build a conditional modifier, or just a way to optionally build two different views?
are we just talking
@ViewBuilder
var myView: some View {
if tappable {
BaseView().padding().<more modifiers>.onTapGesture {
//do something
}
} else {
BaseView().padding().<more modifiers>
}
}
(ignoring the gestureModifier for the sake of an example)
couple of things I don't like about this approach
code repetition
quickly gets unmanageable if there is more than one condition (e.g. 4 paths if two booleans)
or is there something I'm missing that would let me build something more like the if(condition,modifier) with this technique
I'm new to SwiftUI, so I may just not have the feel of it yet, but it does seem like everything is a special case.
I naturally want to be able to treat modifiers generically, and having a generic way to do an if seems like a normal thing to expect in my code.
However it seems that the design of SwiftUI wants me to treat each one as a special case and try to find a way to nullify it.
The view builder language support is still under development, but in time, once we support variable bindings, you should be able to factor that into something like:
@ViewBuilder
var myView: some View {
let view = BaseView().padding().<more modifiers>
if tappable {
view.onTapGesture {
//do something
}
} else {
view
}
}
One way to think about it is that every statement in a view builder that executes, and isn't bound to a variable, is appended to the result view that gets rendered.
I guess the meta question this raises for me is why do I need to wait for you to build a special thing that allows me to conditionally modify a view in a somewhat generic way?
e.g. Why are views special things that need specific language artifacts (like @ViewBuilder) rather than just being amenable to standard swift functionality?
You wouldn't expect to build separate language features to let me modify an Int, change an array, or change the auth logic in an AF.request. What makes Views different fundamental things that need functionality at the language level rather than the code level?
Just to be clearer on the motivation here - it's great to learn about tricks that I can use for different modifiers, but it would be neater to be able to handle a bunch of modifiers like so
(by the way - I can do this with the 'if' function from higher up, though there is some annoying AnyView action going on there)
Essentially, I'm back to generic vs specific here. I want to be able to treat all modifiers in the same way as bits of standard code rather than having a bunch of different special cases. Of course the fact that I want it doesn't have any bearing on reality...
A modifier is really a View that has the modified View as its child, and is thus part of the view hierarchy. SwiftUI intentionally embeds the structure of your view hierarchy into your program’s types for reasons Joe explained in this thread.
It's interesting to read about the motivation here (making it easy to diff/animate the view hierarchy)
It's beyond my skill to understand the tradeoffs here, but I'm still left feeling that (for possibly excellent reasons), we're left with an API where the primary constructs (Views) are second class citizens in the language
If View was a struct or a class, then we can pass it around, write logic, functions etc that work with it. It's a standard thing that I can program with.
e.g. there is nothing controversial or hard about writing
the equivalent with some View is much more tricky as the compiler doesn't allow
func if(condition:Bool,do:(some View)-> some View) -> some View ){...}
it's interesting to know that a modifier is really a View, but not as helpful as that would be if it were a class
Perhaps I'll get used to this. As I say, I'm fairly new to Swift UI. It's kinda magical to use - both in a bad way (confusing, particular, hard to reason about) and a good way (powerful functionality appears at minimal cost)
Is this what you want? This is similar to @cukr's version but avoids the TupleView wonkiness. It doesn't use AnyView either and I think it is the most efficient form (although I'm no expert on SwiftUI so I don't know if it is superior to the 'modifiers with nulled out parameters' way).
extension View {
@ViewBuilder func `if`<T>(_ condition: Bool, transform: (Self) -> T) -> some View where T : View {
if condition {
transform(self)
} else {
self
}
}
}
Individual Views are all struct values, that you can operate on in the usual ways through generic functions over the View protocol. One of the things about SwiftUI, though, is that it really prefers that you always return the exact same type of view. Early versions of SwiftUI did in fact have effectively a single View struct, and we found that this made the animation system in particular hard to use, because it was too easy to write things like:
which seem like they should work, but which are difficult to implement robustly in all cases, since the order of transformations and how they should be individually animated has to be reconstructed for all possible combinations. By basing the design instead on a protocol and generic wrapper structs, SwiftUI is intentionally trying to make the path of least resistance the one that gives the most robust results. The function builder magic is largely unnecessary, and beside the point to some degree; without it, you could still write if as a higher-order function, as @bzamayo noted , and we could construct compound views with regular constructors.