Conditional view modifiers
When working with SwiftUI views, sometimes we would like to apply different modifiers based on conditions/states:
var body: some view {
myView
// if X .padding(8)
// if Y .background(Color.blue)
}
For most cases, SwiftUI offers inert modifiers, where we can pass a different argument value based on the condition:
var body: some view {
myView
.padding(X ? 8 : 0)
.background(Y ? Color.blue : Color.clear)
}
As covered at WWDC21's session Demystify SwiftUI, this is the recommended way, and we should strive to use this approach as much as possible.
However, there are other modifiers, when applying styles for example, where this solution doesn't work:
in this article, let's explore how we can take care of such cases.
if view extension
The most common solution is to define a new if
View
extension:
extension View {
@ViewBuilder
func `if`<Transform: View>(
_ condition: Bool,
transform: (Self) -> Transform
) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
This function will apply transform
to our view when condition
is true
. Otherwise, it will leave the original view untouched.
Going back to our style example, this is one way to use it:
var body: some view {
myView
.if(X) { $0.buttonStyle(.bordered) }
.if(Y) { $0.buttonStyle(.borderedProminent) }
}
If-else view extension
Depending on how compact we want our declarations to be, applying different modifiers based on the condition
true
/false
value would cost us at least two modifiers:
var body: some view {
myView
.if(X) { $0.buttonStyle(.bordered) }
.if(!X) { $0.buttonStyle(.borderedProminent) }
}
This is clear and already succinct, however, if we really want to go all-in with View
extensions, we can define a new if
overload that lets us modify the else
branch as well:
extension View {
@ViewBuilder
func `if`<TrueContent: View, FalseContent: View>(
_ condition: Bool,
if ifTransform: (Self) -> TrueContent,
else elseTransform: (Self) -> FalseContent
) -> some View {
if condition {
ifTransform(self)
} else {
elseTransform(self)
}
}
}
Which will make our example use a single modifier:
var body: some view {
myView
.if(X) { $0.buttonStyle(.bordered) } else: { $0.buttonStyle(.borderedProminent) }
}
If-let view extension
Sometimes we want to apply a modifier only when another value is not nil
, and then use that value in the modifier itself.
In this case we can define a new View
extension that lets us do just that:
extension View {
@ViewBuilder
func ifLet<V, Transform: View>(
_ value: V?,
transform: (Self, V) -> Transform
) -> some View {
if let value = value {
transform(self, value)
} else {
self
}
}
}
The difference from before is that this new function:
- takes in an optional generic
value
V
instead of aBool
condition - passes this generic
value
V
as a parameter of the transform function
Here's an example where a View
applies a foreground color only when optionalColor
is set:
var body: some view {
myView
.ifLet(optionalStyle) { $0.buttonStyle($1) }
}
A word of warning
While these conditional modifiers enable us to apply different things to our views based on conditions, it is very important to understand what happens when the condition changes at run time.
As the view identity changes between the condition branches, switching between them means:
- the current view is destroyed, and another one from the other condition branch is created
- if any view where the conditional modifier is applied holds a state, this state is also destroyed and will be reset to its initial value
- the complete view body is re-evaluated and the view will need to be re-redrawn
- no animations
It would be fairly simple for the SwiftUI team to add such extensions in the library. However, third-party developers would probably use them without realizing the consequences, and we'd quickly find ourselves in some performance pitfalls, with only the framework to blame.
Where is it ok to use these conditional modifiers then? Whenever the condition is static.
Continuing with our style modifier example, we might have a template view that sets a different style based on the user flow. The style won't change at run time during the flow, and the only way for that style to change is to start a new, different flow.
Conclusions
SwiftUI declarative APIs make View
s definition a breeze. When our views need to apply different modifiers based on certain conditions, we should try to use the inert modifier that SwiftUI offers as much as possible.
If we can't use those, we can define and use our conditional view modifiers, letting us keep the same declarativeness that we're accustomed to.
Again, these conditional modifiers work great when the condition value is static. However, they come with a high cost if we need to change them at run time: if you find yourself in this situation, please file a feedback with your use case.
Thank you for reading, and stay tuned for more SwiftUI articles!